mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-09-22 21:30:44 +02:00
Merge branch 'v2' into v3
This commit is contained in:
@@ -22,6 +22,20 @@
|
|||||||
- Upgraded to `store-js@3.0.0-alpha.0`
|
- Upgraded to `store-js@3.0.0-alpha.0`
|
||||||
- Upgraded to `updater-js@3.0.0-alpha.0`
|
- Upgraded to `updater-js@3.0.0-alpha.0`
|
||||||
|
|
||||||
|
## [2.0.45]
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Upgraded to `log-js@2.9.2`
|
||||||
|
- Upgraded to `updater-js@2.12.0`
|
||||||
|
|
||||||
|
## [2.0.44]
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Upgraded to `http-js@2.6.1`
|
||||||
|
- Upgraded to `store-js@2.4.5`
|
||||||
|
|
||||||
## [2.0.43]
|
## [2.0.43]
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|||||||
@@ -24,6 +24,21 @@
|
|||||||
- Upgraded to `haptics@3.0.0-alpha.0`
|
- Upgraded to `haptics@3.0.0-alpha.0`
|
||||||
- Upgraded to `geolocation@3.0.0-alpha.0`
|
- Upgraded to `geolocation@3.0.0-alpha.0`
|
||||||
|
|
||||||
|
## [2.0.49]
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Upgraded to `http@2.7.0`
|
||||||
|
- Upgraded to `log@2.9.2`
|
||||||
|
- Upgraded to `updater@2.12.0`
|
||||||
|
|
||||||
|
## [2.0.48]
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Upgraded to `http@2.6.1`
|
||||||
|
- Upgraded to `store@2.4.5`
|
||||||
|
|
||||||
## [2.0.47]
|
## [2.0.47]
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@
|
|||||||
"rollup": "4.63.1",
|
"rollup": "4.63.1",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"typescript": "6.0.3",
|
"typescript": "6.0.3",
|
||||||
"typescript-eslint": "8.66.0"
|
"typescript-eslint": "8.70.0"
|
||||||
},
|
},
|
||||||
"minimumReleaseAge": 4320,
|
"minimumReleaseAge": 4320,
|
||||||
"packageManager": "pnpm@12.3.4"
|
"packageManager": "pnpm@12.3.4"
|
||||||
|
|||||||
@@ -8,6 +8,28 @@
|
|||||||
|
|
||||||
- Upgraded to `fs-js@3.0.0-alpha.0`
|
- Upgraded to `fs-js@3.0.0-alpha.0`
|
||||||
|
|
||||||
|
## [2.7.0]
|
||||||
|
|
||||||
|
- [`1198a524`](https://github.com/tauri-apps/plugins-workspace/commit/1198a524b710abf2abeb1d9bd7b252402d26ca6d) **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>`.
|
||||||
|
|
||||||
|
## [2.6.1]
|
||||||
|
|
||||||
|
- [`a21555dd`](https://github.com/tauri-apps/plugins-workspace/commit/a21555ddd2eaadfed23848912fe2802c2ba7579e) ([#3566](https://github.com/tauri-apps/plugins-workspace/pull/3566) by [@followdarko](https://github.com/tauri-apps/plugins-workspace/../../followdarko)) Fix unhandled promise rejections on every `fetch` teardown: the request/body cleanup commands were fired as floating promises, and releasing an already-released resource rejects with `The resource id N is invalid.`. `dropBody` is now idempotent and both cleanup calls handle their own rejection.
|
||||||
|
|
||||||
## [2.6.0]
|
## [2.6.0]
|
||||||
|
|
||||||
- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation.
|
- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation.
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ export interface ClientOptions {
|
|||||||
/**
|
/**
|
||||||
* Defines the maximum number of redirects the client should follow.
|
* Defines the maximum number of redirects the client should follow.
|
||||||
* If set to 0, no redirects will be followed.
|
* 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
|
maxRedirections?: number
|
||||||
/** Timeout in milliseconds */
|
/** Timeout in milliseconds */
|
||||||
|
|||||||
+330
-100
@@ -22,6 +22,10 @@ use crate::{
|
|||||||
|
|
||||||
const HTTP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
|
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);
|
struct ReqwestResponse(reqwest::Response);
|
||||||
impl tauri::Resource for ReqwestResponse {}
|
impl tauri::Resource for ReqwestResponse {}
|
||||||
|
|
||||||
@@ -174,6 +178,56 @@ fn attach_proxy(
|
|||||||
Ok(builder)
|
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]
|
#[command]
|
||||||
pub async fn fetch<R: Runtime>(
|
pub async fn fetch<R: Runtime>(
|
||||||
webview: Webview<R>,
|
webview: Webview<R>,
|
||||||
@@ -214,118 +268,115 @@ pub async fn fetch<R: Runtime>(
|
|||||||
|
|
||||||
match scheme {
|
match scheme {
|
||||||
"http" | "https" => {
|
"http" | "https" => {
|
||||||
if Scope::new(
|
let scope = Scope::new(
|
||||||
command_scope
|
command_scope
|
||||||
.allows()
|
.allows()
|
||||||
.iter()
|
.iter()
|
||||||
.chain(global_scope.allows())
|
.chain(global_scope.allows())
|
||||||
|
.cloned()
|
||||||
.collect(),
|
.collect(),
|
||||||
command_scope
|
command_scope
|
||||||
.denies()
|
.denies()
|
||||||
.iter()
|
.iter()
|
||||||
.chain(global_scope.denies())
|
.chain(global_scope.denies())
|
||||||
|
.cloned()
|
||||||
.collect(),
|
.collect(),
|
||||||
)
|
);
|
||||||
.is_allowed(&url)
|
|
||||||
{
|
|
||||||
let mut builder = reqwest::ClientBuilder::new();
|
|
||||||
|
|
||||||
if let Some(danger_config) = danger {
|
if !scope.is_allowed(&url) {
|
||||||
#[cfg(not(feature = "dangerous-settings"))]
|
return Err(Error::UrlNotAllowed(url));
|
||||||
{
|
|
||||||
#[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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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" => {
|
"data" => {
|
||||||
let data_url =
|
let data_url =
|
||||||
@@ -486,3 +537,182 @@ fn is_unsafe_header(header: &HeaderName) -> bool {
|
|||||||
lower.starts_with("proxy-") || lower.starts_with("sec-")
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -43,6 +43,21 @@
|
|||||||
//! - **tracing**: Adds request, response, and cookie-store diagnostics through `tracing`.
|
//! - **tracing**: Adds request, response, and cookie-store diagnostics through `tracing`.
|
||||||
//! - **unsafe-headers**: Allows webview requests to send any headers.
|
//! - **unsafe-headers**: Allows webview requests to send any headers.
|
||||||
//! - **dangerous-settings**: Allows dangerous client settings such as accepting invalid certificates or hostnames.
|
//! - **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;
|
pub use reqwest;
|
||||||
use tauri::{
|
use tauri::{
|
||||||
@@ -50,9 +65,11 @@ use tauri::{
|
|||||||
Manager, Runtime,
|
Manager, Runtime,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub use config::Config;
|
||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
|
|
||||||
mod commands;
|
mod commands;
|
||||||
|
mod config;
|
||||||
mod error;
|
mod error;
|
||||||
#[cfg(feature = "cookies")]
|
#[cfg(feature = "cookies")]
|
||||||
mod reqwest_cookie_store;
|
mod reqwest_cookie_store;
|
||||||
@@ -62,13 +79,14 @@ mod scope;
|
|||||||
const COOKIES_FILENAME: &str = ".cookies";
|
const COOKIES_FILENAME: &str = ".cookies";
|
||||||
|
|
||||||
pub(crate) struct Http {
|
pub(crate) struct Http {
|
||||||
|
pub(crate) config: Config,
|
||||||
#[cfg(feature = "cookies")]
|
#[cfg(feature = "cookies")]
|
||||||
cookies_jar: std::sync::Arc<crate::reqwest_cookie_store::CookieStoreMutex>,
|
cookies_jar: std::sync::Arc<crate::reqwest_cookie_store::CookieStoreMutex>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<Config>> {
|
||||||
Builder::<R>::new("http")
|
Builder::<R, Option<Config>>::new("http")
|
||||||
.setup(|app, _| {
|
.setup(|app, api| {
|
||||||
#[cfg(feature = "cookies")]
|
#[cfg(feature = "cookies")]
|
||||||
let cookies_jar = {
|
let cookies_jar = {
|
||||||
use crate::reqwest_cookie_store::*;
|
use crate::reqwest_cookie_store::*;
|
||||||
@@ -96,6 +114,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let state = Http {
|
let state = Http {
|
||||||
|
config: api.config().clone().unwrap_or_default(),
|
||||||
#[cfg(feature = "cookies")]
|
#[cfg(feature = "cookies")]
|
||||||
cookies_jar: std::sync::Arc::new(cookies_jar),
|
cookies_jar: std::sync::Arc::new(cookies_jar),
|
||||||
};
|
};
|
||||||
|
|||||||
+26
-27
@@ -59,16 +59,16 @@ impl<'de> Deserialize<'de> for Entry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scope for filesystem access.
|
/// Scope for HTTP access.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Scope<'a> {
|
pub struct Scope {
|
||||||
allowed: Vec<&'a Arc<Entry>>,
|
allowed: Vec<Arc<Entry>>,
|
||||||
denied: Vec<&'a Arc<Entry>>,
|
denied: Vec<Arc<Entry>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Scope<'a> {
|
impl Scope {
|
||||||
/// Creates a new scope from the scope configuration.
|
/// 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 }
|
Self { allowed, denied }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,25 +94,24 @@ impl<'a> Scope<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
impl std::str::FromStr for Entry {
|
||||||
use std::{str::FromStr, sync::Arc};
|
type Err = urlpattern::quirks::Error;
|
||||||
|
|
||||||
use super::Entry;
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let url = parse_url_pattern(s)?;
|
||||||
impl FromStr for Entry {
|
Ok(Self { url })
|
||||||
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 })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn denied_takes_precedence() {
|
fn denied_takes_precedence() {
|
||||||
let allow = Arc::new("http://localhost:8080/file.png".parse().unwrap());
|
let allow = Arc::new("http://localhost:8080/file.png".parse().unwrap());
|
||||||
let deny = Arc::new("http://localhost:8080/*".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/file.png".parse().unwrap()));
|
||||||
assert!(!scope.is_allowed(&"http://localhost:8080?framework=tauri".parse().unwrap()));
|
assert!(!scope.is_allowed(&"http://localhost:8080?framework=tauri".parse().unwrap()));
|
||||||
}
|
}
|
||||||
@@ -121,7 +120,7 @@ mod tests {
|
|||||||
fn fixed_url() {
|
fn fixed_url() {
|
||||||
// plain URL
|
// plain URL
|
||||||
let entry = Arc::new("http://localhost:8080".parse().unwrap());
|
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/".parse().unwrap()));
|
assert!(scope.is_allowed(&"http://localhost:8080/".parse().unwrap()));
|
||||||
assert!(scope.is_allowed(&"http://localhost:8080/file".parse().unwrap()));
|
assert!(scope.is_allowed(&"http://localhost:8080/file".parse().unwrap()));
|
||||||
@@ -137,7 +136,7 @@ mod tests {
|
|||||||
fn fixed_path() {
|
fn fixed_path() {
|
||||||
// URL with fixed path
|
// URL with fixed path
|
||||||
let entry = Arc::new("http://localhost:8080/file.png".parse().unwrap());
|
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".parse().unwrap()));
|
||||||
assert!(scope.is_allowed(&"http://localhost:8080/file.png?q=1".parse().unwrap()));
|
assert!(scope.is_allowed(&"http://localhost:8080/file.png?q=1".parse().unwrap()));
|
||||||
@@ -150,7 +149,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn pattern_wildcard() {
|
fn pattern_wildcard() {
|
||||||
let entry = Arc::new("http://localhost:8080/*.png".parse().unwrap());
|
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".parse().unwrap()));
|
||||||
assert!(scope.is_allowed(&"http://localhost:8080/file.png#head".parse().unwrap()));
|
assert!(scope.is_allowed(&"http://localhost:8080/file.png#head".parse().unwrap()));
|
||||||
@@ -167,7 +166,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn domain_wildcard() {
|
fn domain_wildcard() {
|
||||||
let entry = Arc::new("http://*".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".parse().unwrap()));
|
||||||
assert!(scope.is_allowed(&"http://something.else#tauri".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()));
|
assert!(!scope.is_allowed(&"https://something.else".parse().unwrap()));
|
||||||
|
|
||||||
let entry = Arc::new("http://*/*".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".parse().unwrap()));
|
||||||
assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap()));
|
assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap()));
|
||||||
@@ -191,7 +190,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn scheme_wildcard() {
|
fn scheme_wildcard() {
|
||||||
let entry = Arc::new("*://*".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".parse().unwrap()));
|
||||||
assert!(scope.is_allowed(&"http://something.else/path/to/file".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()));
|
assert!(scope.is_allowed(&"https://something.else?x=1#frag".parse().unwrap()));
|
||||||
|
|
||||||
let entry = Arc::new("*://*/*".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".parse().unwrap()));
|
||||||
assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap()));
|
assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap()));
|
||||||
@@ -212,7 +211,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn validate_query() {
|
fn validate_query() {
|
||||||
let entry = Arc::new("https://tauri.app/path?x=*".parse().unwrap());
|
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()));
|
assert!(scope.is_allowed(&"https://tauri.app/path?x=5".parse().unwrap()));
|
||||||
|
|
||||||
@@ -222,7 +221,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn validate_hash() {
|
fn validate_hash() {
|
||||||
let entry = Arc::new("https://tauri.app/path#frame*".parse().unwrap());
|
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()));
|
assert!(scope.is_allowed(&"https://tauri.app/path#frame".parse().unwrap()));
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
|
|
||||||
- [`363438b5`](https://github.com/tauri-apps/plugins-workspace/commit/363438b50a09162379d57c7c7bfc132520c1e5d4) Update to tauri 3.0 alpha.
|
- [`363438b5`](https://github.com/tauri-apps/plugins-workspace/commit/363438b50a09162379d57c7c7bfc132520c1e5d4) Update to tauri 3.0 alpha.
|
||||||
|
|
||||||
|
## [2.9.2]
|
||||||
|
|
||||||
|
- [`2df3d936`](https://github.com/tauri-apps/plugins-workspace/commit/2df3d9368187e1d2ce736d3e0b2d4bdcae35b011) ([#3574](https://github.com/tauri-apps/plugins-workspace/pull/3574)) Log webview log target locations after 2 colons instead of 1. (e.g. `webview:myFunction@http://localhost:5173/src/myFile.ts?t=1787480723620:47:4` to `webview::myFunction@http://localhost:5173/src/myFile.ts?t=1787480723620:47:4`)
|
||||||
|
|
||||||
## [2.9.1]
|
## [2.9.1]
|
||||||
|
|
||||||
- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation.
|
- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation.
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ pub fn log(
|
|||||||
let level = log::Level::from(level);
|
let level = log::Level::from(level);
|
||||||
|
|
||||||
let target = if let Some(location) = location {
|
let target = if let Some(location) = location {
|
||||||
format!("{WEBVIEW_TARGET}:{location}")
|
format!("{WEBVIEW_TARGET}::{location}")
|
||||||
} else {
|
} else {
|
||||||
WEBVIEW_TARGET.to_string()
|
WEBVIEW_TARGET.to_owned()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut builder = RecordBuilder::new();
|
let mut builder = RecordBuilder::new();
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
|
|
||||||
- Upgraded to `deep-link@3.0.0-alpha.0`
|
- Upgraded to `deep-link@3.0.0-alpha.0`
|
||||||
|
|
||||||
|
## [2.4.5]
|
||||||
|
|
||||||
|
- [`67cd25a1`](https://github.com/tauri-apps/plugins-workspace/commit/67cd25a10ce3deb1b935e725228e528766f24ad7) ([#3592](https://github.com/tauri-apps/plugins-workspace/pull/3592)) On Windows, the second instance now allows the first instance to bring its window to the front before exiting, so focusing a window from the callback no longer gets refused by Windows.
|
||||||
|
|
||||||
## [2.4.4]
|
## [2.4.4]
|
||||||
|
|
||||||
- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation.
|
- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation.
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ use windows_sys::Win32::{
|
|||||||
Threading::{CreateMutexW, ReleaseMutex},
|
Threading::{CreateMutexW, ReleaseMutex},
|
||||||
},
|
},
|
||||||
UI::WindowsAndMessaging::{
|
UI::WindowsAndMessaging::{
|
||||||
self as w32wm, CreateWindowExW, DefWindowProcW, DestroyWindow, FindWindowW,
|
self as w32wm, AllowSetForegroundWindow, CreateWindowExW, DefWindowProcW, DestroyWindow,
|
||||||
RegisterClassExW, SendMessageW, CREATESTRUCTW, GWLP_USERDATA, GWL_STYLE,
|
FindWindowW, GetWindowThreadProcessId, RegisterClassExW, SendMessageW, CREATESTRUCTW,
|
||||||
WINDOW_LONG_PTR_INDEX, WM_COPYDATA, WM_CREATE, WM_DESTROY, WNDCLASSEXW, WS_EX_LAYERED,
|
GWLP_USERDATA, GWL_STYLE, WINDOW_LONG_PTR_INDEX, WM_COPYDATA, WM_CREATE, WM_DESTROY,
|
||||||
WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE,
|
WNDCLASSEXW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT,
|
||||||
|
WS_OVERLAPPED, WS_POPUP, WS_VISIBLE,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,6 +75,16 @@ pub fn init<R: Runtime>(callback: Box<SingleInstanceCallback<R>>) -> TauriPlugin
|
|||||||
let hwnd = FindWindowW(class_name.as_ptr(), window_name.as_ptr());
|
let hwnd = FindWindowW(class_name.as_ptr(), window_name.as_ptr());
|
||||||
|
|
||||||
if !hwnd.is_null() {
|
if !hwnd.is_null() {
|
||||||
|
// Windows lets us bring a window to the front, but not the first
|
||||||
|
// instance. Hand that right over before we exit, so focusing a window
|
||||||
|
// from the callback works. Windows takes it back if the user switches
|
||||||
|
// to another app in the meantime.
|
||||||
|
let mut pid = 0;
|
||||||
|
GetWindowThreadProcessId(hwnd, &mut pid);
|
||||||
|
if pid != 0 {
|
||||||
|
AllowSetForegroundWindow(pid);
|
||||||
|
}
|
||||||
|
|
||||||
let cwd = std::env::current_dir().unwrap_or_default();
|
let cwd = std::env::current_dir().unwrap_or_default();
|
||||||
let cwd = cwd.to_str().unwrap_or_default();
|
let cwd = cwd.to_str().unwrap_or_default();
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
|
|
||||||
- [`363438b5`](https://github.com/tauri-apps/plugins-workspace/commit/363438b50a09162379d57c7c7bfc132520c1e5d4) Update to tauri 3.0 alpha.
|
- [`363438b5`](https://github.com/tauri-apps/plugins-workspace/commit/363438b50a09162379d57c7c7bfc132520c1e5d4) Update to tauri 3.0 alpha.
|
||||||
|
|
||||||
|
## [2.4.5]
|
||||||
|
|
||||||
|
- [`c050e073`](https://github.com/tauri-apps/plugins-workspace/commit/c050e073b68205bae7389fb86958cb9331e1a3be) ([#3572](https://github.com/tauri-apps/plugins-workspace/pull/3572) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix `apply_pending_auto_save` can deadlock the store
|
||||||
|
|
||||||
## \[2.4.4]
|
## \[2.4.4]
|
||||||
|
|
||||||
- [`2ed6d6c1`](https://github.com/tauri-apps/plugins-workspace/commit/2ed6d6c1de4f5051b87cb6a2c5ac45320c444989) ([#3499](https://github.com/tauri-apps/plugins-workspace/pull/3499) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix `StoreOptions` requires `defaults` field
|
- [`2ed6d6c1`](https://github.com/tauri-apps/plugins-workspace/commit/2ed6d6c1de4f5051b87cb6a2c5ac45320c444989) ([#3499](https://github.com/tauri-apps/plugins-workspace/pull/3499) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix `StoreOptions` requires `defaults` field
|
||||||
|
|||||||
@@ -600,7 +600,8 @@ impl<R: Runtime> Store<R> {
|
|||||||
|
|
||||||
fn apply_pending_auto_save(&self) {
|
fn apply_pending_auto_save(&self) {
|
||||||
// Cancel and save if auto save is pending
|
// Cancel and save if auto save is pending
|
||||||
if let Some(sender) = self.auto_save_debounce_sender.lock().unwrap().take() {
|
let auto_save_debounce_sender = self.auto_save_debounce_sender.lock().unwrap().take();
|
||||||
|
if let Some(sender) = auto_save_debounce_sender {
|
||||||
let _ = sender.send(AutoSaveMessage::Cancel);
|
let _ = sender.send(AutoSaveMessage::Cancel);
|
||||||
let _ = self.save();
|
let _ = self.save();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,24 @@
|
|||||||
|
|
||||||
- [`363438b5`](https://github.com/tauri-apps/plugins-workspace/commit/363438b50a09162379d57c7c7bfc132520c1e5d4) Update to tauri 3.0 alpha.
|
- [`363438b5`](https://github.com/tauri-apps/plugins-workspace/commit/363438b50a09162379d57c7c7bfc132520c1e5d4) Update to tauri 3.0 alpha.
|
||||||
|
|
||||||
|
## [2.12.0]
|
||||||
|
|
||||||
|
- [`1308bfa3`](https://github.com/tauri-apps/plugins-workspace/commit/1308bfa399b962b3977c767100a6339d1cbfdd20) **Breaking change:** the `allowDowngrades` option was removed from the `check` command and is now read from the plugin configuration instead.
|
||||||
|
|
||||||
|
Previously any code running in the webview could pass `allowDowngrades: true` to `plugin:updater|check` and relax the version check from "the update must be newer" to "the update must be different", overriding the comparator the application had configured on the Rust side. The flag is now an application-level setting:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plugins": {
|
||||||
|
"updater": {
|
||||||
|
"allowDowngrades": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It defaults to `false`, and is ignored when the application provides its own `Builder::default_version_comparator`, which continues to take precedence.
|
||||||
|
|
||||||
## [2.11.0]
|
## [2.11.0]
|
||||||
|
|
||||||
- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation.
|
- [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation.
|
||||||
@@ -219,3 +237,25 @@
|
|||||||
## \[2.0.0-alpha.0]
|
## \[2.0.0-alpha.0]
|
||||||
|
|
||||||
- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||||
|
7c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.12.
|
||||||
|
|
||||||
|
## \[2.0.0-alpha.3]
|
||||||
|
|
||||||
|
- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to @tauri-apps/api v2.0.0-alpha.11.
|
||||||
|
|
||||||
|
## \[2.0.0-alpha.2]
|
||||||
|
|
||||||
|
- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9.
|
||||||
|
|
||||||
|
## \[2.0.0-alpha.2]
|
||||||
|
|
||||||
|
- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12.
|
||||||
|
|
||||||
|
## \[2.0.0-alpha.1]
|
||||||
|
|
||||||
|
- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11.
|
||||||
|
- [`4ab90f0`](https://github.com/tauri-apps/plugins-workspace/commit/4ab90f048eab2918344f97dc8e04413a404e392d)([#431](https://github.com/tauri-apps/plugins-workspace/pull/431)) The updater plugin is recieving a few changes to improve consistency and ergonomics of the Rust and JS APIs
|
||||||
|
|
||||||
|
## \[2.0.0-alpha.0]
|
||||||
|
|
||||||
|
- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
|
||||||
|
|||||||
@@ -22,10 +22,6 @@ interface CheckOptions {
|
|||||||
* Target identifier for the running application. This is sent to the backend.
|
* Target identifier for the running application. This is sent to the backend.
|
||||||
*/
|
*/
|
||||||
target?: string
|
target?: string
|
||||||
/**
|
|
||||||
* Allow downgrades to previous versions by not checking if the current version is greater than the available version.
|
|
||||||
*/
|
|
||||||
allowDowngrades?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Options used when downloading an update */
|
/** Options used when downloading an update */
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ pub(crate) async fn check<R: Runtime>(
|
|||||||
timeout: Option<u64>,
|
timeout: Option<u64>,
|
||||||
proxy: Option<String>,
|
proxy: Option<String>,
|
||||||
target: Option<String>,
|
target: Option<String>,
|
||||||
allow_downgrades: Option<bool>,
|
|
||||||
) -> Result<Option<Metadata>> {
|
) -> Result<Option<Metadata>> {
|
||||||
let mut builder = webview.updater_builder();
|
let mut builder = webview.updater_builder();
|
||||||
if let Some(headers) = headers {
|
if let Some(headers) = headers {
|
||||||
@@ -64,9 +63,6 @@ pub(crate) async fn check<R: Runtime>(
|
|||||||
if let Some(target) = target {
|
if let Some(target) = target {
|
||||||
builder = builder.target(target);
|
builder = builder.target(target);
|
||||||
}
|
}
|
||||||
if allow_downgrades.unwrap_or(false) {
|
|
||||||
builder = builder.version_comparator(|current, update| update.version != current);
|
|
||||||
}
|
|
||||||
|
|
||||||
let updater = builder.build()?;
|
let updater = builder.build()?;
|
||||||
let update = updater.check().await?;
|
let update = updater.check().await?;
|
||||||
|
|||||||
@@ -114,6 +114,38 @@ pub struct Config {
|
|||||||
pub endpoints: Vec<Url>,
|
pub endpoints: Vec<Url>,
|
||||||
/// Signature public key.
|
/// Signature public key.
|
||||||
pub pubkey: String,
|
pub pubkey: String,
|
||||||
|
/// Require the update signature to carry the version it was signed for, and reject the
|
||||||
|
/// update when that version differs from the one announced by the update endpoint.
|
||||||
|
///
|
||||||
|
/// The endpoint response is fetched over TLS but is not itself signed, and the signature
|
||||||
|
/// only covers the downloaded artifact. Without this flag, anyone able to serve a crafted
|
||||||
|
/// response can pair an inflated `version` field with the `url` and `signature` of an
|
||||||
|
/// older release and force a downgrade to a genuine but outdated build, since that older
|
||||||
|
/// artifact carries a valid signature.
|
||||||
|
///
|
||||||
|
/// The signed version is read from the signature's trusted comment, which is covered by
|
||||||
|
/// the signature. Releases signed before the Tauri CLI started recording it carry no
|
||||||
|
/// version, so enabling this rejects them. Re-sign and re-publish every release your users
|
||||||
|
/// can still update from before turning this on.
|
||||||
|
///
|
||||||
|
/// This is checked independently of the version comparison: it constrains which artifact a
|
||||||
|
/// given version number may resolve to, not whether that version is newer.
|
||||||
|
///
|
||||||
|
/// The default value of this flag is `false`.
|
||||||
|
pub require_signed_version: bool,
|
||||||
|
/// Allow the updater to install a release whose version is not newer than the
|
||||||
|
/// currently running one, changing the version check from "must be newer" to
|
||||||
|
/// "must be different".
|
||||||
|
///
|
||||||
|
/// Note that the updater only verifies the signature of the downloaded artifact,
|
||||||
|
/// not the version advertised by the update endpoint, so enabling this removes the
|
||||||
|
/// only guard against installing a previously released (and validly signed) version.
|
||||||
|
///
|
||||||
|
/// Ignored when the application sets a custom
|
||||||
|
/// [`Builder::default_version_comparator`](crate::Builder::default_version_comparator).
|
||||||
|
///
|
||||||
|
/// The default value of this flag is `false`.
|
||||||
|
pub allow_downgrades: bool,
|
||||||
/// The Windows configuration for the updater.
|
/// The Windows configuration for the updater.
|
||||||
pub windows: Option<WindowsConfig>,
|
pub windows: Option<WindowsConfig>,
|
||||||
}
|
}
|
||||||
@@ -135,6 +167,10 @@ impl<'de> Deserialize<'de> for Config {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub endpoints: Vec<Url>,
|
pub endpoints: Vec<Url>,
|
||||||
pub pubkey: String,
|
pub pubkey: String,
|
||||||
|
#[serde(default, alias = "require-signed-version")]
|
||||||
|
pub require_signed_version: bool,
|
||||||
|
#[serde(default, alias = "allow-downgrades")]
|
||||||
|
pub allow_downgrades: bool,
|
||||||
pub windows: Option<WindowsConfig>,
|
pub windows: Option<WindowsConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +188,8 @@ impl<'de> Deserialize<'de> for Config {
|
|||||||
dangerous_accept_invalid_hostnames: config.dangerous_accept_invalid_hostnames,
|
dangerous_accept_invalid_hostnames: config.dangerous_accept_invalid_hostnames,
|
||||||
endpoints: config.endpoints,
|
endpoints: config.endpoints,
|
||||||
pubkey: config.pubkey,
|
pubkey: config.pubkey,
|
||||||
|
require_signed_version: config.require_signed_version,
|
||||||
|
allow_downgrades: config.allow_downgrades,
|
||||||
windows: config.windows,
|
windows: config.windows,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,22 @@ pub enum Error {
|
|||||||
/// The configured updater endpoint must use a secure protocol like `https`
|
/// The configured updater endpoint must use a secure protocol like `https`
|
||||||
#[error("The configured updater endpoint must use a secure protocol like `https`.")]
|
#[error("The configured updater endpoint must use a secure protocol like `https`.")]
|
||||||
InsecureTransportProtocol,
|
InsecureTransportProtocol,
|
||||||
|
/// The version the artifact was signed for does not match the version announced by the
|
||||||
|
/// update endpoint.
|
||||||
|
#[error(
|
||||||
|
"The update was signed for version {signed} but the update endpoint announced version {announced}. The endpoint response may have been tampered with to force installing a different release."
|
||||||
|
)]
|
||||||
|
SignedVersionMismatch {
|
||||||
|
/// The version read from the signature's trusted comment.
|
||||||
|
signed: String,
|
||||||
|
/// The version announced by the update endpoint.
|
||||||
|
announced: String,
|
||||||
|
},
|
||||||
|
/// `requireSignedVersion` is enabled but the signature does not carry a version.
|
||||||
|
#[error(
|
||||||
|
"The update signature does not specify the version it was signed for, which `requireSignedVersion` requires. Re-sign and re-publish this release, or disable `requireSignedVersion`."
|
||||||
|
)]
|
||||||
|
MissingSignedVersion,
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Tauri(#[from] tauri::Error),
|
Tauri(#[from] tauri::Error),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,11 @@ impl<R: Runtime, T: Manager<R>> UpdaterExt<R> for T {
|
|||||||
|
|
||||||
builder.version_comparator = version_comparator.clone();
|
builder.version_comparator = version_comparator.clone();
|
||||||
|
|
||||||
|
// a comparator set by the application takes precedence over the configuration
|
||||||
|
if builder.version_comparator.is_none() && config.allow_downgrades {
|
||||||
|
builder = builder.version_comparator(|current, update| update.version != current);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
target_os = "linux",
|
target_os = "linux",
|
||||||
target_os = "dragonfly",
|
target_os = "dragonfly",
|
||||||
|
|||||||
@@ -737,7 +737,13 @@ impl Update {
|
|||||||
}
|
}
|
||||||
on_download_finish();
|
on_download_finish();
|
||||||
|
|
||||||
verify_signature(&buffer, &self.signature, &self.context.config.pubkey)?;
|
verify_signature(
|
||||||
|
&buffer,
|
||||||
|
&self.signature,
|
||||||
|
&self.context.config.pubkey,
|
||||||
|
&self.version,
|
||||||
|
self.context.config.require_signed_version,
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(buffer)
|
Ok(buffer)
|
||||||
}
|
}
|
||||||
@@ -1521,7 +1527,13 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate signature
|
// Validate signature
|
||||||
fn verify_signature(data: &[u8], release_signature: &str, pub_key: &str) -> Result<()> {
|
fn verify_signature(
|
||||||
|
data: &[u8],
|
||||||
|
release_signature: &str,
|
||||||
|
pub_key: &str,
|
||||||
|
announced_version: &str,
|
||||||
|
require_signed_version: bool,
|
||||||
|
) -> Result<()> {
|
||||||
// we need to convert the pub key
|
// we need to convert the pub key
|
||||||
let pub_key_decoded = base64_to_string(pub_key)?;
|
let pub_key_decoded = base64_to_string(pub_key)?;
|
||||||
let public_key = PublicKey::decode(&pub_key_decoded)?;
|
let public_key = PublicKey::decode(&pub_key_decoded)?;
|
||||||
@@ -1530,7 +1542,65 @@ fn verify_signature(data: &[u8], release_signature: &str, pub_key: &str) -> Resu
|
|||||||
|
|
||||||
// Validate signature or bail out
|
// Validate signature or bail out
|
||||||
public_key.verify(data, &signature, true)?;
|
public_key.verify(data, &signature, true)?;
|
||||||
Ok(())
|
|
||||||
|
// Only now is the trusted comment usable: minisign's global signature covers it, and
|
||||||
|
// `verify` above is what checks that global signature. Reading it before this point would
|
||||||
|
// be trusting attacker controlled data.
|
||||||
|
verify_signed_version(
|
||||||
|
signature.trusted_comment(),
|
||||||
|
announced_version,
|
||||||
|
require_signed_version,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks the version the artifact was signed for against the version the update endpoint
|
||||||
|
/// announced.
|
||||||
|
///
|
||||||
|
/// The endpoint response is not signed, so its `version` field on its own does not prove which
|
||||||
|
/// release the `url` and `signature` actually point at. Comparing it against the signed version
|
||||||
|
/// is what stops a tampered response from pairing a new version number with an older release.
|
||||||
|
fn verify_signed_version(
|
||||||
|
trusted_comment: &str,
|
||||||
|
announced_version: &str,
|
||||||
|
require_signed_version: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let Some(signed_version) = signed_version(trusted_comment) else {
|
||||||
|
// Signatures produced before the Tauri CLI started recording the version carry none, so
|
||||||
|
// this can only be enforced when the app opts in. Note that leaving it off means an
|
||||||
|
// attacker can bypass the check outright by serving one of those older signatures.
|
||||||
|
return if require_signed_version {
|
||||||
|
Err(Error::MissingSignedVersion)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// compare as semver so that equivalent spellings like `1.2.3` and `v1.2.3` match, falling
|
||||||
|
// back to a literal comparison for versions that are not valid semver
|
||||||
|
let matches = match (
|
||||||
|
Version::from_str(signed_version.trim_start_matches('v')),
|
||||||
|
Version::from_str(announced_version.trim_start_matches('v')),
|
||||||
|
) {
|
||||||
|
(Ok(signed), Ok(announced)) => signed == announced,
|
||||||
|
_ => signed_version == announced_version,
|
||||||
|
};
|
||||||
|
|
||||||
|
if matches {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::SignedVersionMismatch {
|
||||||
|
signed: signed_version.to_string(),
|
||||||
|
announced: announced_version.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the `version` field out of a signature's trusted comment, which the Tauri CLI writes as
|
||||||
|
/// tab separated `key:value` pairs, e.g. `timestamp:1700000000\tfile:app.tar.gz\tversion:1.2.3`.
|
||||||
|
fn signed_version(trusted_comment: &str) -> Option<&str> {
|
||||||
|
trusted_comment
|
||||||
|
.split('\t')
|
||||||
|
.find_map(|field| field.strip_prefix("version:"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn base64_to_string(base64_string: &str) -> Result<String> {
|
fn base64_to_string(base64_string: &str) -> Result<String> {
|
||||||
@@ -1636,6 +1706,59 @@ fn escape_msi_property_arg(arg: impl AsRef<OsStr>) -> String {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::{signed_version, verify_signed_version};
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
const CURRENT: &str = "timestamp:1700000000\tfile:app_1.2.3_x64.msi.zip\tversion:1.2.3";
|
||||||
|
// signatures produced before the CLI started embedding the version
|
||||||
|
const LEGACY: &str = "timestamp:1600000000\tfile:app_1.0.0_x64.msi.zip";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reads_the_signed_version() {
|
||||||
|
assert_eq!(signed_version(CURRENT), Some("1.2.3"));
|
||||||
|
assert_eq!(signed_version(LEGACY), None);
|
||||||
|
// must not match on a field that merely ends in `version:`
|
||||||
|
assert_eq!(signed_version("timestamp:1\tfile:app-version:2.zip"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accepts_a_matching_version() {
|
||||||
|
assert!(verify_signed_version(CURRENT, "1.2.3", true).is_ok());
|
||||||
|
assert!(verify_signed_version(CURRENT, "1.2.3", false).is_ok());
|
||||||
|
// the endpoint and the CLI may spell the same version differently
|
||||||
|
assert!(verify_signed_version(CURRENT, "v1.2.3", true).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_a_version_the_artifact_was_not_signed_for() {
|
||||||
|
// the rollback the flag exists to stop: an old artifact announced as a new version
|
||||||
|
let err = verify_signed_version(CURRENT, "9.9.9", false).unwrap_err();
|
||||||
|
assert!(
|
||||||
|
matches!(err, Error::SignedVersionMismatch { ref signed, ref announced }
|
||||||
|
if signed == "1.2.3" && announced == "9.9.9"),
|
||||||
|
"unexpected error: {err}"
|
||||||
|
);
|
||||||
|
// rejected regardless of whether the app opted in, since the signature does say
|
||||||
|
// which version it covers
|
||||||
|
assert!(verify_signed_version(CURRENT, "9.9.9", true).is_err());
|
||||||
|
assert!(verify_signed_version(CURRENT, "1.2.4", true).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_requires_a_signed_version_when_configured() {
|
||||||
|
assert!(verify_signed_version(LEGACY, "9.9.9", false).is_ok());
|
||||||
|
assert!(matches!(
|
||||||
|
verify_signed_version(LEGACY, "9.9.9", true).unwrap_err(),
|
||||||
|
Error::MissingSignedVersion
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compares_non_semver_versions_literally() {
|
||||||
|
let comment = "timestamp:1700000000\tfile:app.zip\tversion:2024-01-01";
|
||||||
|
assert!(verify_signed_version(comment, "2024-01-01", true).is_ok());
|
||||||
|
assert!(verify_signed_version(comment, "2024-01-02", true).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
|
|||||||
@@ -9,12 +9,16 @@ use std::{
|
|||||||
fs::File,
|
fs::File,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::Command,
|
process::Command,
|
||||||
sync::Arc,
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tauri::utils::config::{Updater, V1Compatible};
|
use tauri::utils::config::{Updater, V1Compatible};
|
||||||
|
|
||||||
|
/// Every test here drives `cargo tauri build` against the same crate and target directory, and
|
||||||
|
/// binds a fixed port, so they cannot overlap.
|
||||||
|
static BUILD_LOCK: Mutex<()> = Mutex::new(());
|
||||||
|
|
||||||
const UPDATER_PRIVATE_KEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5TlFOMFpXYzJFOUdjeHJEVXY4WE1TMUxGNDJVUjNrMmk1WlR3UVJVUWwva0FBQkFBQUFBQUFBQUFBQUlBQUFBQUpVK3ZkM3R3eWhyN3hiUXhQb2hvWFVzUW9FbEs3NlNWYjVkK1F2VGFRU1FEaGxuRUtlell5U0gxYS9DbVRrS0YyZVJGblhjeXJibmpZeGJjS0ZKSUYwYndYc2FCNXpHalM3MHcrODMwN3kwUG9SOWpFNVhCSUd6L0E4TGRUT096TEtLR1JwT1JEVFU9Cg==";
|
const UPDATER_PRIVATE_KEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5TlFOMFpXYzJFOUdjeHJEVXY4WE1TMUxGNDJVUjNrMmk1WlR3UVJVUWwva0FBQkFBQUFBQUFBQUFBQUlBQUFBQUpVK3ZkM3R3eWhyN3hiUXhQb2hvWFVzUW9FbEs3NlNWYjVkK1F2VGFRU1FEaGxuRUtlell5U0gxYS9DbVRrS0YyZVJGblhjeXJibmpZeGJjS0ZKSUYwYndYc2FCNXpHalM3MHcrODMwN3kwUG9SOWpFNVhCSUd6L0E4TGRUT096TEtLR1JwT1JEVFU9Cg==";
|
||||||
const UPDATED_EXIT_CODE: i32 = 0;
|
const UPDATED_EXIT_CODE: i32 = 0;
|
||||||
const ERROR_EXIT_CODE: i32 = 1;
|
const ERROR_EXIT_CODE: i32 = 1;
|
||||||
@@ -24,6 +28,10 @@ const UP_TO_DATE_EXIT_CODE: i32 = 2;
|
|||||||
struct Config {
|
struct Config {
|
||||||
version: &'static str,
|
version: &'static str,
|
||||||
bundle: BundleConfig,
|
bundle: BundleConfig,
|
||||||
|
/// Merged into `plugins` of the app's `tauri.conf.json`. Only set by the signed version
|
||||||
|
/// tests, which need to flip `requireSignedVersion` and point at their own server.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
plugins: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -35,13 +43,13 @@ struct BundleConfig {
|
|||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct PlatformUpdate {
|
struct PlatformUpdate {
|
||||||
signature: String,
|
signature: String,
|
||||||
url: &'static str,
|
url: String,
|
||||||
with_elevated_task: bool,
|
with_elevated_task: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct Update {
|
struct Update {
|
||||||
version: &'static str,
|
version: String,
|
||||||
date: String,
|
date: String,
|
||||||
platforms: HashMap<String, PlatformUpdate>,
|
platforms: HashMap<String, PlatformUpdate>,
|
||||||
}
|
}
|
||||||
@@ -56,6 +64,12 @@ fn build_app(cwd: &Path, config: &Config, target: Option<BundleTarget>) {
|
|||||||
.env("TAURI_SIGNING_PRIVATE_KEY_PASSWORD", "")
|
.env("TAURI_SIGNING_PRIVATE_KEY_PASSWORD", "")
|
||||||
.current_dir(cwd);
|
.current_dir(cwd);
|
||||||
|
|
||||||
|
// linuxdeploy ships a `strip` that does not know the `.relr.dyn` sections recent distros emit,
|
||||||
|
// and it aborts the whole bundle when a strip call fails. Nothing here benefits from a smaller
|
||||||
|
// AppImage, so skip it.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
command.env("NO_STRIP", "1");
|
||||||
|
|
||||||
if let Some(target) = target {
|
if let Some(target) = target {
|
||||||
command.arg("--bundles").arg(target.name());
|
command.arg("--bundles").arg(target.name());
|
||||||
} else {
|
} else {
|
||||||
@@ -107,6 +121,61 @@ impl Default for BundleTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Copies a bundle out of the bundler output directory, returning the path to run.
|
||||||
|
///
|
||||||
|
/// Every bundler wipes its output before writing — the AppImage one removes `bundle/appimage`
|
||||||
|
/// entirely, the macOS one the `.app` — so the app built for the initial version only survives
|
||||||
|
/// until the next `build_app` call. On Linux and macOS the update is also installed over the very
|
||||||
|
/// bundle being run, which would otherwise leave a 1.0.0 app behind in the bundler output.
|
||||||
|
///
|
||||||
|
/// Windows drives the executable `cargo build` leaves in `target/release`, which no bundler
|
||||||
|
/// touches, so there is nothing to copy there.
|
||||||
|
fn stage_app_under_test(root_dir: &Path, target: &str) -> PathBuf {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let _ = target;
|
||||||
|
return root_dir.join("target/release/app-updater.exe");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
let bundle_path = test_cases(root_dir, "0.1.0", target.to_string())
|
||||||
|
.first()
|
||||||
|
.unwrap()
|
||||||
|
.1
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let staging_dir = root_dir.join("target/release/app-under-test");
|
||||||
|
let _ = std::fs::remove_dir_all(&staging_dir);
|
||||||
|
std::fs::create_dir_all(&staging_dir).expect("failed to create the staging directory");
|
||||||
|
|
||||||
|
let staged = staging_dir.join(bundle_path.file_name().unwrap());
|
||||||
|
copy_recursively(&bundle_path, &staged).unwrap_or_else(|e| {
|
||||||
|
panic!(
|
||||||
|
"failed to copy {} to {}: {e}",
|
||||||
|
bundle_path.display(),
|
||||||
|
staged.display()
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
return staged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies a file, or a directory such as a macOS `.app`, preserving permissions.
|
||||||
|
fn copy_recursively(from: &Path, to: &Path) -> std::io::Result<()> {
|
||||||
|
if from.is_dir() {
|
||||||
|
std::fs::create_dir_all(to)?;
|
||||||
|
for entry in std::fs::read_dir(from)? {
|
||||||
|
let entry = entry?;
|
||||||
|
copy_recursively(&entry.path(), &to.join(entry.file_name()))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
std::fs::copy(from, to).map(|_| ())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn target_to_platforms(
|
fn target_to_platforms(
|
||||||
update_platform: Option<String>,
|
update_platform: Option<String>,
|
||||||
signature: String,
|
signature: String,
|
||||||
@@ -117,7 +186,7 @@ fn target_to_platforms(
|
|||||||
platform,
|
platform,
|
||||||
PlatformUpdate {
|
PlatformUpdate {
|
||||||
signature,
|
signature,
|
||||||
url: "http://localhost:3007/download",
|
url: "http://localhost:3007/download".into(),
|
||||||
with_elevated_task: false,
|
with_elevated_task: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -270,6 +339,8 @@ fn test_cases(
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn update_app() {
|
fn update_app() {
|
||||||
|
let _lock = BUILD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
|
||||||
let target =
|
let target =
|
||||||
tauri_plugin_updater::target().expect("running updater test in an unsupported platform");
|
tauri_plugin_updater::target().expect("running updater test in an unsupported platform");
|
||||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
@@ -281,12 +352,14 @@ fn update_app() {
|
|||||||
bundle: BundleConfig {
|
bundle: BundleConfig {
|
||||||
create_updater_artifacts: Updater::Bool(true),
|
create_updater_artifacts: Updater::Bool(true),
|
||||||
},
|
},
|
||||||
|
plugins: None,
|
||||||
},
|
},
|
||||||
Config {
|
Config {
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
bundle: BundleConfig {
|
bundle: BundleConfig {
|
||||||
create_updater_artifacts: Updater::String(V1Compatible::V1Compatible),
|
create_updater_artifacts: Updater::String(V1Compatible::V1Compatible),
|
||||||
},
|
},
|
||||||
|
plugins: None,
|
||||||
},
|
},
|
||||||
] {
|
] {
|
||||||
let v1_compatible = matches!(
|
let v1_compatible = matches!(
|
||||||
@@ -359,7 +432,7 @@ fn update_app() {
|
|||||||
target_to_platforms(update_platform.clone(), signature.clone());
|
target_to_platforms(update_platform.clone(), signature.clone());
|
||||||
|
|
||||||
let body = serde_json::to_vec(&Update {
|
let body = serde_json::to_vec(&Update {
|
||||||
version: "1.0.0",
|
version: "1.0.0".into(),
|
||||||
date: time::OffsetDateTime::now_utc()
|
date: time::OffsetDateTime::now_utc()
|
||||||
.format(&time::format_description::well_known::Rfc3339)
|
.format(&time::format_description::well_known::Rfc3339)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
@@ -393,41 +466,38 @@ fn update_app() {
|
|||||||
|
|
||||||
config.version = "0.1.0";
|
config.version = "0.1.0";
|
||||||
|
|
||||||
// bundle initial app version
|
// bundle initial app version; Linux and macOS run the bundle itself, so it cannot be
|
||||||
build_app(&manifest_dir, &config, None);
|
// skipped there
|
||||||
|
build_app(
|
||||||
|
&manifest_dir,
|
||||||
|
&config,
|
||||||
|
if cfg!(windows) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(bundle_target)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let app_path = stage_app_under_test(&root_dir, &target);
|
||||||
|
|
||||||
for expected_exit_code in status_checks {
|
for expected_exit_code in status_checks {
|
||||||
let mut binary_cmd = if cfg!(windows) {
|
let mut binary_cmd = if cfg!(target_os = "macos") {
|
||||||
Command::new(root_dir.join("target/release/app-updater.exe"))
|
Command::new(app_path.join("Contents/MacOS/app-updater"))
|
||||||
} else if cfg!(target_os = "macos") {
|
} else if cfg!(target_os = "linux")
|
||||||
Command::new(
|
&& std::env::var("CI").map(|v| v == "true").unwrap_or_default()
|
||||||
test_cases(&root_dir, "0.1.0", target.clone())
|
{
|
||||||
.first()
|
|
||||||
.unwrap()
|
|
||||||
.1
|
|
||||||
.join("Contents/MacOS/app-updater"),
|
|
||||||
)
|
|
||||||
} else if std::env::var("CI").map(|v| v == "true").unwrap_or_default() {
|
|
||||||
let mut c = Command::new("xvfb-run");
|
let mut c = Command::new("xvfb-run");
|
||||||
c.arg("--auto-servernum").arg(
|
c.arg("--auto-servernum").arg(&app_path);
|
||||||
&test_cases(&root_dir, "0.1.0", target.clone())
|
|
||||||
.first()
|
|
||||||
.unwrap()
|
|
||||||
.1,
|
|
||||||
);
|
|
||||||
c
|
c
|
||||||
} else {
|
} else {
|
||||||
Command::new(
|
Command::new(&app_path)
|
||||||
&test_cases(&root_dir, "0.1.0", target.clone())
|
|
||||||
.first()
|
|
||||||
.unwrap()
|
|
||||||
.1,
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
binary_cmd.env("TARGET", bundle_target.name());
|
binary_cmd.env("TARGET", bundle_target.name());
|
||||||
|
|
||||||
let status = binary_cmd.status().expect("failed to run app");
|
let status = binary_cmd
|
||||||
|
.status()
|
||||||
|
.unwrap_or_else(|e| panic!("failed to run {}: {e}", app_path.display()));
|
||||||
let code = status.code().unwrap_or(-1);
|
let code = status.code().unwrap_or(-1);
|
||||||
|
|
||||||
if code != expected_exit_code {
|
if code != expected_exit_code {
|
||||||
@@ -447,3 +517,222 @@ fn update_app() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SIGNED_VERSION_PORT: u16 = 3008;
|
||||||
|
/// Fragment of `Error::SignedVersionMismatch`. The app prints the updater error before exiting,
|
||||||
|
/// and a rejected update exits with the same code as a failed install, so the message is what
|
||||||
|
/// tells them apart.
|
||||||
|
const MISMATCH_ERROR: &str = "was signed for version";
|
||||||
|
|
||||||
|
struct ServedUpdate {
|
||||||
|
version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app_binary(root_dir: &Path) -> PathBuf {
|
||||||
|
root_dir.join(if cfg!(windows) {
|
||||||
|
"target/release/app-updater.exe"
|
||||||
|
} else {
|
||||||
|
"target/release/app-updater"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the app against the update server, restoring the binary from `pristine` first.
|
||||||
|
///
|
||||||
|
/// A case that clears the version check goes on to actually install, and on Linux installing means
|
||||||
|
/// writing the downloaded bytes over the running executable. Every case therefore has to start
|
||||||
|
/// from an untouched binary, or the first one that passes leaves the update behind as the app.
|
||||||
|
fn run_app(root_dir: &Path, pristine: &Path) -> String {
|
||||||
|
let binary = app_binary(root_dir);
|
||||||
|
std::fs::copy(pristine, &binary).expect("failed to restore the app binary");
|
||||||
|
|
||||||
|
let mut command = if cfg!(target_os = "linux")
|
||||||
|
&& std::env::var("CI").map(|v| v == "true").unwrap_or_default()
|
||||||
|
{
|
||||||
|
let mut c = Command::new("xvfb-run");
|
||||||
|
c.arg("--auto-servernum").arg(&binary);
|
||||||
|
c
|
||||||
|
} else {
|
||||||
|
Command::new(&binary)
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = command.output().expect("failed to run app");
|
||||||
|
String::from_utf8_lossy(&output.stdout).into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The update endpoint response is not signed, so its `version` field alone does not prove which
|
||||||
|
/// release the `url` and `signature` point at. This bundles a genuine 1.0.0 release and then varies
|
||||||
|
/// only the version the endpoint announces for it, which is the shape of a forced downgrade: a
|
||||||
|
/// tampered response pairing a new version number with an older release's signature.
|
||||||
|
///
|
||||||
|
/// The check runs in `Update::download`, before anything is installed, so a rejected case never
|
||||||
|
/// reaches the installer while an accepted one does. Both exit non-zero, which is why these assert
|
||||||
|
/// on the error message rather than the exit code.
|
||||||
|
#[test]
|
||||||
|
fn update_validates_signed_version() {
|
||||||
|
let _lock = BUILD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
|
||||||
|
let target =
|
||||||
|
tauri_plugin_updater::target().expect("running updater test in an unsupported platform");
|
||||||
|
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let root_dir = manifest_dir.join("../../../..");
|
||||||
|
|
||||||
|
// bundle the release the endpoint will point at; the CLI records 1.0.0 in its signature
|
||||||
|
let bundle_target = BundleTarget::default();
|
||||||
|
build_app(
|
||||||
|
&manifest_dir,
|
||||||
|
&Config {
|
||||||
|
version: "1.0.0",
|
||||||
|
bundle: BundleConfig {
|
||||||
|
create_updater_artifacts: Updater::Bool(true),
|
||||||
|
},
|
||||||
|
plugins: None,
|
||||||
|
},
|
||||||
|
Some(bundle_target),
|
||||||
|
);
|
||||||
|
|
||||||
|
let out_bundle_path = test_cases(&root_dir, "1.0.0", target.clone())
|
||||||
|
.first()
|
||||||
|
.unwrap()
|
||||||
|
.1
|
||||||
|
.clone();
|
||||||
|
let updater_extension = {
|
||||||
|
let bundle_ext = out_bundle_path
|
||||||
|
.extension()
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
if cfg!(target_os = "macos") {
|
||||||
|
format!("{bundle_ext}.tar.gz")
|
||||||
|
} else {
|
||||||
|
bundle_ext
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let signature =
|
||||||
|
std::fs::read_to_string(out_bundle_path.with_extension(format!("{updater_extension}.sig")))
|
||||||
|
.expect("failed to read signature file");
|
||||||
|
|
||||||
|
// move it aside so building the running app cannot clobber it
|
||||||
|
let out_updater_path = out_bundle_path.with_extension(&updater_extension);
|
||||||
|
let updater_path = root_dir.join(format!(
|
||||||
|
"target/release/{}",
|
||||||
|
out_updater_path.file_name().unwrap().to_str().unwrap()
|
||||||
|
));
|
||||||
|
std::fs::rename(&out_updater_path, &updater_path).expect("failed to rename bundle");
|
||||||
|
|
||||||
|
let served = Arc::new(Mutex::new(ServedUpdate {
|
||||||
|
version: "1.0.0".into(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let server = Arc::new(
|
||||||
|
tiny_http::Server::http(format!("localhost:{SIGNED_VERSION_PORT}"))
|
||||||
|
.expect("failed to start updater server"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let server_ = server.clone();
|
||||||
|
let served_ = served.clone();
|
||||||
|
let target_ = target.clone();
|
||||||
|
let updater_path_ = updater_path.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
for request in server_.incoming_requests() {
|
||||||
|
match request.url() {
|
||||||
|
"/" => {
|
||||||
|
let mut platforms = HashMap::new();
|
||||||
|
platforms.insert(
|
||||||
|
target_.clone(),
|
||||||
|
PlatformUpdate {
|
||||||
|
// always the genuine 1.0.0 signature; only the version above it moves
|
||||||
|
signature: signature.clone(),
|
||||||
|
url: format!("http://localhost:{SIGNED_VERSION_PORT}/download"),
|
||||||
|
with_elevated_task: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let body = serde_json::to_vec(&Update {
|
||||||
|
version: served_.lock().unwrap().version.clone(),
|
||||||
|
date: time::OffsetDateTime::now_utc()
|
||||||
|
.format(&time::format_description::well_known::Rfc3339)
|
||||||
|
.unwrap(),
|
||||||
|
platforms,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let len = body.len();
|
||||||
|
let _ = request.respond(tiny_http::Response::new(
|
||||||
|
tiny_http::StatusCode(200),
|
||||||
|
Vec::new(),
|
||||||
|
std::io::Cursor::new(body),
|
||||||
|
Some(len),
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"/download" => {
|
||||||
|
let _ = request.respond(tiny_http::Response::from_file(
|
||||||
|
File::open(&updater_path_).unwrap_or_else(|_| {
|
||||||
|
panic!("failed to open updater bundle {}", updater_path_.display())
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// `requireSignedVersion` is baked in by `generate_context!`, so each value needs its own build
|
||||||
|
let pristine = root_dir.join("target/release/app-updater.pristine");
|
||||||
|
let build = |require_signed_version: bool| {
|
||||||
|
build_app(
|
||||||
|
&manifest_dir,
|
||||||
|
&Config {
|
||||||
|
version: "0.1.0",
|
||||||
|
bundle: BundleConfig {
|
||||||
|
create_updater_artifacts: Updater::Bool(true),
|
||||||
|
},
|
||||||
|
plugins: Some(serde_json::json!({
|
||||||
|
"updater": {
|
||||||
|
"endpoints": [format!("http://localhost:{SIGNED_VERSION_PORT}")],
|
||||||
|
"requireSignedVersion": require_signed_version,
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
std::fs::copy(app_binary(&root_dir), &pristine)
|
||||||
|
.expect("failed to keep a pristine copy of the app binary");
|
||||||
|
};
|
||||||
|
|
||||||
|
let check = |announced: &str, expected: Option<&str>, unexpected: Option<&str>| {
|
||||||
|
served.lock().unwrap().version = announced.to_string();
|
||||||
|
let output = run_app(&root_dir, &pristine);
|
||||||
|
|
||||||
|
if let Some(expected) = expected {
|
||||||
|
assert!(
|
||||||
|
output.contains(expected),
|
||||||
|
"expected {expected:?} when announcing {announced} for a 1.0.0 signature, got: {output}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(unexpected) = unexpected {
|
||||||
|
assert!(
|
||||||
|
!output.contains(unexpected),
|
||||||
|
"unexpected {unexpected:?} when announcing {announced} for a 1.0.0 signature, got: {output}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
build(true);
|
||||||
|
|
||||||
|
// the rollback this exists to stop: a 1.0.0 release dressed up as a newer one
|
||||||
|
check("1.5.0", Some(MISMATCH_ERROR), None);
|
||||||
|
// the announced version is what the artifact was signed for, so it must go through
|
||||||
|
check("1.0.0", None, Some(MISMATCH_ERROR));
|
||||||
|
|
||||||
|
build(false);
|
||||||
|
|
||||||
|
// a signature that names a version is held to it whether or not the option is on
|
||||||
|
check("1.5.0", Some(MISMATCH_ERROR), None);
|
||||||
|
|
||||||
|
server.unblock();
|
||||||
|
|
||||||
|
// leave a runnable binary behind rather than whichever update was installed last
|
||||||
|
let _ = std::fs::copy(&pristine, app_binary(&root_dir));
|
||||||
|
let _ = std::fs::remove_file(&pristine);
|
||||||
|
}
|
||||||
|
|||||||
Generated
+82
-79
@@ -105,6 +105,9 @@ settings:
|
|||||||
autoInstallPeers: true
|
autoInstallPeers: true
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
overrides:
|
||||||
|
devalue@<5.9.1: ^5.9.1
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
@@ -143,8 +146,8 @@ importers:
|
|||||||
specifier: 6.0.3
|
specifier: 6.0.3
|
||||||
version: 6.0.3
|
version: 6.0.3
|
||||||
typescript-eslint:
|
typescript-eslint:
|
||||||
specifier: 8.66.0
|
specifier: 8.70.0
|
||||||
version: 8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)
|
version: 8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)
|
||||||
|
|
||||||
examples/api:
|
examples/api:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -210,7 +213,7 @@ importers:
|
|||||||
version: link:../../plugins/upload
|
version: link:../../plugins/upload
|
||||||
'@zerodevx/svelte-json-view':
|
'@zerodevx/svelte-json-view':
|
||||||
specifier: 2.0.0
|
specifier: 2.0.0
|
||||||
version: 2.0.0(svelte@5.55.7(@typescript-eslint/types@8.66.0))
|
version: 2.0.0(svelte@5.55.7(@typescript-eslint/types@8.70.0))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@iconify-json/codicon':
|
'@iconify-json/codicon':
|
||||||
specifier: ^1.2.49
|
specifier: ^1.2.49
|
||||||
@@ -220,7 +223,7 @@ importers:
|
|||||||
version: 1.2.2
|
version: 1.2.2
|
||||||
'@sveltejs/vite-plugin-svelte':
|
'@sveltejs/vite-plugin-svelte':
|
||||||
specifier: ^7.0.0
|
specifier: ^7.0.0
|
||||||
version: 7.0.0(svelte@5.55.7(@typescript-eslint/types@8.66.0))(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1))
|
version: 7.0.0(svelte@5.55.7(@typescript-eslint/types@8.70.0))(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1))
|
||||||
'@tauri-apps/cli':
|
'@tauri-apps/cli':
|
||||||
specifier: 3.0.0-alpha.1
|
specifier: 3.0.0-alpha.1
|
||||||
version: 3.0.0-alpha.1
|
version: 3.0.0-alpha.1
|
||||||
@@ -229,7 +232,7 @@ importers:
|
|||||||
version: 66.6.7
|
version: 66.6.7
|
||||||
svelte:
|
svelte:
|
||||||
specifier: ^5.55.7
|
specifier: ^5.55.7
|
||||||
version: 5.55.7(@typescript-eslint/types@8.66.0)
|
version: 5.55.7(@typescript-eslint/types@8.70.0)
|
||||||
unocss:
|
unocss:
|
||||||
specifier: ^66.6.7
|
specifier: ^66.6.7
|
||||||
version: 66.6.7(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1))
|
version: 66.6.7(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1))
|
||||||
@@ -1253,63 +1256,63 @@ packages:
|
|||||||
'@types/trusted-types@2.0.7':
|
'@types/trusted-types@2.0.7':
|
||||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.66.0':
|
'@typescript-eslint/eslint-plugin@8.70.0':
|
||||||
resolution: {integrity: sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==}
|
resolution: {integrity: sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@typescript-eslint/parser': ^8.66.0
|
'@typescript-eslint/parser': ^8.70.0
|
||||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
typescript: '>=4.8.4 <6.1.0'
|
typescript: '>=4.8.4 <6.1.0'
|
||||||
|
|
||||||
'@typescript-eslint/parser@8.66.0':
|
'@typescript-eslint/parser@8.70.0':
|
||||||
resolution: {integrity: sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==}
|
resolution: {integrity: sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
typescript: '>=4.8.4 <6.1.0'
|
typescript: '>=4.8.4 <6.1.0'
|
||||||
|
|
||||||
'@typescript-eslint/project-service@8.66.0':
|
'@typescript-eslint/project-service@8.70.0':
|
||||||
resolution: {integrity: sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==}
|
resolution: {integrity: sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.8.4 <6.1.0'
|
typescript: '>=4.8.4 <6.1.0'
|
||||||
|
|
||||||
'@typescript-eslint/scope-manager@8.66.0':
|
'@typescript-eslint/scope-manager@8.70.0':
|
||||||
resolution: {integrity: sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==}
|
resolution: {integrity: sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@typescript-eslint/tsconfig-utils@8.66.0':
|
'@typescript-eslint/tsconfig-utils@8.70.0':
|
||||||
resolution: {integrity: sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==}
|
resolution: {integrity: sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.8.4 <6.1.0'
|
typescript: '>=4.8.4 <6.1.0'
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.66.0':
|
'@typescript-eslint/type-utils@8.70.0':
|
||||||
resolution: {integrity: sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==}
|
resolution: {integrity: sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
typescript: '>=4.8.4 <6.1.0'
|
typescript: '>=4.8.4 <6.1.0'
|
||||||
|
|
||||||
'@typescript-eslint/types@8.66.0':
|
'@typescript-eslint/types@8.70.0':
|
||||||
resolution: {integrity: sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==}
|
resolution: {integrity: sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@typescript-eslint/typescript-estree@8.66.0':
|
'@typescript-eslint/typescript-estree@8.70.0':
|
||||||
resolution: {integrity: sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==}
|
resolution: {integrity: sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.8.4 <6.1.0'
|
typescript: '>=4.8.4 <6.1.0'
|
||||||
|
|
||||||
'@typescript-eslint/utils@8.66.0':
|
'@typescript-eslint/utils@8.70.0':
|
||||||
resolution: {integrity: sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==}
|
resolution: {integrity: sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
typescript: '>=4.8.4 <6.1.0'
|
typescript: '>=4.8.4 <6.1.0'
|
||||||
|
|
||||||
'@typescript-eslint/visitor-keys@8.66.0':
|
'@typescript-eslint/visitor-keys@8.70.0':
|
||||||
resolution: {integrity: sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==}
|
resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@unocss/cli@66.6.7':
|
'@unocss/cli@66.6.7':
|
||||||
@@ -1483,8 +1486,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
devalue@5.8.1:
|
devalue@5.9.4:
|
||||||
resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==}
|
resolution: {integrity: sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==}
|
||||||
|
|
||||||
duplexer@0.1.2:
|
duplexer@0.1.2:
|
||||||
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
|
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
|
||||||
@@ -1973,8 +1976,8 @@ packages:
|
|||||||
type-level-regexp@0.1.17:
|
type-level-regexp@0.1.17:
|
||||||
resolution: {integrity: sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==}
|
resolution: {integrity: sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==}
|
||||||
|
|
||||||
typescript-eslint@8.66.0:
|
typescript-eslint@8.70.0:
|
||||||
resolution: {integrity: sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==}
|
resolution: {integrity: sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
@@ -2531,12 +2534,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.16.0
|
acorn: 8.16.0
|
||||||
|
|
||||||
'@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.7(@typescript-eslint/types@8.66.0))(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1))':
|
'@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.7(@typescript-eslint/types@8.70.0))(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
deepmerge: 4.3.1
|
deepmerge: 4.3.1
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
obug: 2.1.1
|
obug: 2.1.1
|
||||||
svelte: 5.55.7(@typescript-eslint/types@8.66.0)
|
svelte: 5.55.7(@typescript-eslint/types@8.70.0)
|
||||||
vite: 8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1)
|
vite: 8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1)
|
||||||
vitefu: 1.1.2(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1))
|
vitefu: 1.1.2(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1))
|
||||||
|
|
||||||
@@ -2604,14 +2607,14 @@ snapshots:
|
|||||||
|
|
||||||
'@types/trusted-types@2.0.7': {}
|
'@types/trusted-types@2.0.7': {}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)':
|
'@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
'@typescript-eslint/parser': 8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)
|
'@typescript-eslint/parser': 8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)
|
||||||
'@typescript-eslint/scope-manager': 8.66.0
|
'@typescript-eslint/scope-manager': 8.70.0
|
||||||
'@typescript-eslint/type-utils': 8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)
|
'@typescript-eslint/type-utils': 8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)
|
||||||
'@typescript-eslint/utils': 8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)
|
'@typescript-eslint/utils': 8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.66.0
|
'@typescript-eslint/visitor-keys': 8.70.0
|
||||||
eslint: 10.4.0(jiti@2.6.1)
|
eslint: 10.4.0(jiti@2.6.1)
|
||||||
ignore: 7.0.5
|
ignore: 7.0.5
|
||||||
natural-compare: 1.4.0
|
natural-compare: 1.4.0
|
||||||
@@ -2620,41 +2623,41 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/parser@8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)':
|
'@typescript-eslint/parser@8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/scope-manager': 8.66.0
|
'@typescript-eslint/scope-manager': 8.70.0
|
||||||
'@typescript-eslint/types': 8.66.0
|
'@typescript-eslint/types': 8.70.0
|
||||||
'@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3)
|
'@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.66.0
|
'@typescript-eslint/visitor-keys': 8.70.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
eslint: 10.4.0(jiti@2.6.1)
|
eslint: 10.4.0(jiti@2.6.1)
|
||||||
typescript: 6.0.3
|
typescript: 6.0.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/project-service@8.66.0(typescript@6.0.3)':
|
'@typescript-eslint/project-service@8.70.0(typescript@6.0.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3)
|
'@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3)
|
||||||
'@typescript-eslint/types': 8.66.0
|
'@typescript-eslint/types': 8.70.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
typescript: 6.0.3
|
typescript: 6.0.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/scope-manager@8.66.0':
|
'@typescript-eslint/scope-manager@8.70.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.66.0
|
'@typescript-eslint/types': 8.70.0
|
||||||
'@typescript-eslint/visitor-keys': 8.66.0
|
'@typescript-eslint/visitor-keys': 8.70.0
|
||||||
|
|
||||||
'@typescript-eslint/tsconfig-utils@8.66.0(typescript@6.0.3)':
|
'@typescript-eslint/tsconfig-utils@8.70.0(typescript@6.0.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 6.0.3
|
typescript: 6.0.3
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)':
|
'@typescript-eslint/type-utils@8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.66.0
|
'@typescript-eslint/types': 8.70.0
|
||||||
'@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3)
|
'@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3)
|
||||||
'@typescript-eslint/utils': 8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)
|
'@typescript-eslint/utils': 8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
eslint: 10.4.0(jiti@2.6.1)
|
eslint: 10.4.0(jiti@2.6.1)
|
||||||
ts-api-utils: 2.5.0(typescript@6.0.3)
|
ts-api-utils: 2.5.0(typescript@6.0.3)
|
||||||
@@ -2662,14 +2665,14 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/types@8.66.0': {}
|
'@typescript-eslint/types@8.70.0': {}
|
||||||
|
|
||||||
'@typescript-eslint/typescript-estree@8.66.0(typescript@6.0.3)':
|
'@typescript-eslint/typescript-estree@8.70.0(typescript@6.0.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/project-service': 8.66.0(typescript@6.0.3)
|
'@typescript-eslint/project-service': 8.70.0(typescript@6.0.3)
|
||||||
'@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3)
|
'@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3)
|
||||||
'@typescript-eslint/types': 8.66.0
|
'@typescript-eslint/types': 8.70.0
|
||||||
'@typescript-eslint/visitor-keys': 8.66.0
|
'@typescript-eslint/visitor-keys': 8.70.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
minimatch: 10.2.4
|
minimatch: 10.2.4
|
||||||
semver: 7.7.4
|
semver: 7.7.4
|
||||||
@@ -2679,20 +2682,20 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/utils@8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)':
|
'@typescript-eslint/utils@8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.6.1))
|
'@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.6.1))
|
||||||
'@typescript-eslint/scope-manager': 8.66.0
|
'@typescript-eslint/scope-manager': 8.70.0
|
||||||
'@typescript-eslint/types': 8.66.0
|
'@typescript-eslint/types': 8.70.0
|
||||||
'@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3)
|
'@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3)
|
||||||
eslint: 10.4.0(jiti@2.6.1)
|
eslint: 10.4.0(jiti@2.6.1)
|
||||||
typescript: 6.0.3
|
typescript: 6.0.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/visitor-keys@8.66.0':
|
'@typescript-eslint/visitor-keys@8.70.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.66.0
|
'@typescript-eslint/types': 8.70.0
|
||||||
eslint-visitor-keys: 5.0.1
|
eslint-visitor-keys: 5.0.1
|
||||||
|
|
||||||
'@unocss/cli@66.6.7':
|
'@unocss/cli@66.6.7':
|
||||||
@@ -2829,9 +2832,9 @@ snapshots:
|
|||||||
unplugin-utils: 0.3.1
|
unplugin-utils: 0.3.1
|
||||||
vite: 8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1)
|
vite: 8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1)
|
||||||
|
|
||||||
'@zerodevx/svelte-json-view@2.0.0(svelte@5.55.7(@typescript-eslint/types@8.66.0))':
|
'@zerodevx/svelte-json-view@2.0.0(svelte@5.55.7(@typescript-eslint/types@8.70.0))':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
svelte: 5.55.7(@typescript-eslint/types@8.66.0)
|
svelte: 5.55.7(@typescript-eslint/types@8.70.0)
|
||||||
|
|
||||||
acorn-jsx@5.3.2(acorn@8.16.0):
|
acorn-jsx@5.3.2(acorn@8.16.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2899,7 +2902,7 @@ snapshots:
|
|||||||
|
|
||||||
detect-libc@2.1.2: {}
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
devalue@5.8.1: {}
|
devalue@5.9.4: {}
|
||||||
|
|
||||||
duplexer@0.1.2: {}
|
duplexer@0.1.2: {}
|
||||||
|
|
||||||
@@ -3003,11 +3006,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
estraverse: 5.3.0
|
estraverse: 5.3.0
|
||||||
|
|
||||||
esrap@2.2.8(@typescript-eslint/types@8.66.0):
|
esrap@2.2.8(@typescript-eslint/types@8.70.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/types': 8.66.0
|
'@typescript-eslint/types': 8.70.0
|
||||||
|
|
||||||
esrecurse@4.3.0:
|
esrecurse@4.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3389,7 +3392,7 @@ snapshots:
|
|||||||
|
|
||||||
supports-preserve-symlinks-flag@1.0.0: {}
|
supports-preserve-symlinks-flag@1.0.0: {}
|
||||||
|
|
||||||
svelte@5.55.7(@typescript-eslint/types@8.66.0):
|
svelte@5.55.7(@typescript-eslint/types@8.70.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/remapping': 2.3.5
|
'@jridgewell/remapping': 2.3.5
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
@@ -3400,9 +3403,9 @@ snapshots:
|
|||||||
aria-query: 5.3.1
|
aria-query: 5.3.1
|
||||||
axobject-query: 4.1.0
|
axobject-query: 4.1.0
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
devalue: 5.8.1
|
devalue: 5.9.4
|
||||||
esm-env: 1.2.2
|
esm-env: 1.2.2
|
||||||
esrap: 2.2.8(@typescript-eslint/types@8.66.0)
|
esrap: 2.2.8(@typescript-eslint/types@8.70.0)
|
||||||
is-reference: 3.0.3
|
is-reference: 3.0.3
|
||||||
locate-character: 3.0.0
|
locate-character: 3.0.0
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
@@ -3445,12 +3448,12 @@ snapshots:
|
|||||||
|
|
||||||
type-level-regexp@0.1.17: {}
|
type-level-regexp@0.1.17: {}
|
||||||
|
|
||||||
typescript-eslint@8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3):
|
typescript-eslint@8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)
|
'@typescript-eslint/eslint-plugin': 8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)
|
||||||
'@typescript-eslint/parser': 8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)
|
'@typescript-eslint/parser': 8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)
|
||||||
'@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3)
|
'@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3)
|
||||||
'@typescript-eslint/utils': 8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)
|
'@typescript-eslint/utils': 8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)
|
||||||
eslint: 10.4.0(jiti@2.6.1)
|
eslint: 10.4.0(jiti@2.6.1)
|
||||||
typescript: 6.0.3
|
typescript: 6.0.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
|||||||
@@ -20,3 +20,6 @@ minimumReleaseAgeExclude:
|
|||||||
- '@tauri-apps/cli-win32-x64-msvc@3.0.0-alpha.0'
|
- '@tauri-apps/cli-win32-x64-msvc@3.0.0-alpha.0'
|
||||||
- '@tauri-apps/cli@3.0.0-alpha.0'
|
- '@tauri-apps/cli@3.0.0-alpha.0'
|
||||||
- '@tauri-apps/api@3.0.0-alpha.1'
|
- '@tauri-apps/api@3.0.0-alpha.1'
|
||||||
|
|
||||||
|
overrides:
|
||||||
|
devalue@<5.9.1: ^5.9.1
|
||||||
|
|||||||
Reference in New Issue
Block a user