fix(desktop): preserve remote proxy base paths

Keep proxied requests under the configured remote base path and fail fast when the local HTTPS proxy certificate cannot be trusted, instead of starting a broken HTTPS proxy.
This commit is contained in:
pascalandr
2026-04-18 11:36:26 +02:00
parent be4f383602
commit efe5c455e0
2 changed files with 83 additions and 3 deletions

View File

@@ -309,7 +309,9 @@ async fn open_remote_window(app: AppHandle, payload: RemoteWindowPayload) -> Res
let tls_config = match cert_manager::ensure_local_cert() {
Ok(local_cert) => {
if let Err(err) = cert_manager::trust_cert_in_store(&local_cert.cert_der) {
eprintln!("[tauri] failed to trust proxy cert: {err}");
return Err(format!(
"Failed to trust the local proxy certificate. Accept the certificate installation prompt and try again: {err}"
));
}
Some(ProxyTlsConfig {
cert_pem: local_cert.cert_pem,

View File

@@ -188,11 +188,40 @@ async fn proxy_request(
fn build_upstream_url(base_url: &Url, uri: &Uri) -> Result<Url, url::ParseError> {
let mut url = base_url.clone();
url.set_path(uri.path());
url.set_path(&rewrite_request_path(base_url, uri.path()));
url.set_query(strip_proxy_token_query(uri.query()).as_deref());
Ok(url)
}
fn rewrite_request_path(base_url: &Url, request_path: &str) -> String {
let base_path = normalized_base_path(base_url);
if base_path == "/" {
return request_path.to_string();
}
if request_path == "/" {
return base_path.to_string();
}
if path_has_base_prefix(base_path, request_path) {
return request_path.to_string();
}
format!("{base_path}{request_path}")
}
fn normalized_base_path(base_url: &Url) -> &str {
let path = base_url.path();
if path.is_empty() { "/" } else { path }
}
fn path_has_base_prefix(base_path: &str, request_path: &str) -> bool {
request_path == base_path
|| request_path
.strip_prefix(base_path)
.is_some_and(|suffix| suffix.starts_with('/'))
}
fn generate_session_token() -> String {
let mut bytes = [0_u8; 16];
rand::thread_rng().fill_bytes(&mut bytes);
@@ -284,7 +313,7 @@ fn rewrite_referer_header(headers: &HeaderMap, target_base_url: &Url) -> Option<
let parsed = Url::parse(referer).ok()?;
let mut rewritten = target_base_url.clone();
rewritten.set_path(parsed.path());
rewritten.set_path(&rewrite_request_path(target_base_url, parsed.path()));
rewritten.set_query(parsed.query());
rewritten.set_fragment(parsed.fragment());
Some(rewritten.to_string())
@@ -380,3 +409,52 @@ fn is_hop_by_hop_header(name: &HeaderName) -> bool {
})
.contains(name.as_str())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_upstream_url_prefixes_root_relative_requests_under_base_path() {
let base = Url::parse("https://example.com/app").unwrap();
let uri = "/api/auth/status?foo=bar".parse::<Uri>().unwrap();
let upstream = build_upstream_url(&base, &uri).unwrap();
assert_eq!(upstream.as_str(), "https://example.com/app/api/auth/status?foo=bar");
}
#[test]
fn build_upstream_url_keeps_requests_already_under_base_path() {
let base = Url::parse("https://example.com/app").unwrap();
let uri = "/app/api/auth/status?foo=bar".parse::<Uri>().unwrap();
let upstream = build_upstream_url(&base, &uri).unwrap();
assert_eq!(upstream.as_str(), "https://example.com/app/api/auth/status?foo=bar");
}
#[test]
fn build_upstream_url_maps_root_to_base_path() {
let base = Url::parse("https://example.com/app").unwrap();
let uri = "/".parse::<Uri>().unwrap();
let upstream = build_upstream_url(&base, &uri).unwrap();
assert_eq!(upstream.as_str(), "https://example.com/app");
}
#[test]
fn rewrite_referer_header_prefixes_root_relative_path_under_base_path() {
let target = Url::parse("https://example.com/app").unwrap();
let mut headers = HeaderMap::new();
headers.insert(
axum::http::header::REFERER,
HeaderValue::from_static("https://127.0.0.1:3000/api/auth/status?foo=bar"),
);
let referer = rewrite_referer_header(&headers, &target).unwrap();
assert_eq!(referer, "https://example.com/app/api/auth/status?foo=bar");
}
}