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:
@@ -309,7 +309,9 @@ async fn open_remote_window(app: AppHandle, payload: RemoteWindowPayload) -> Res
|
|||||||
let tls_config = match cert_manager::ensure_local_cert() {
|
let tls_config = match cert_manager::ensure_local_cert() {
|
||||||
Ok(local_cert) => {
|
Ok(local_cert) => {
|
||||||
if let Err(err) = cert_manager::trust_cert_in_store(&local_cert.cert_der) {
|
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 {
|
Some(ProxyTlsConfig {
|
||||||
cert_pem: local_cert.cert_pem,
|
cert_pem: local_cert.cert_pem,
|
||||||
|
|||||||
@@ -188,11 +188,40 @@ async fn proxy_request(
|
|||||||
|
|
||||||
fn build_upstream_url(base_url: &Url, uri: &Uri) -> Result<Url, url::ParseError> {
|
fn build_upstream_url(base_url: &Url, uri: &Uri) -> Result<Url, url::ParseError> {
|
||||||
let mut url = base_url.clone();
|
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());
|
url.set_query(strip_proxy_token_query(uri.query()).as_deref());
|
||||||
Ok(url)
|
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 {
|
fn generate_session_token() -> String {
|
||||||
let mut bytes = [0_u8; 16];
|
let mut bytes = [0_u8; 16];
|
||||||
rand::thread_rng().fill_bytes(&mut bytes);
|
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 parsed = Url::parse(referer).ok()?;
|
||||||
|
|
||||||
let mut rewritten = target_base_url.clone();
|
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_query(parsed.query());
|
||||||
rewritten.set_fragment(parsed.fragment());
|
rewritten.set_fragment(parsed.fragment());
|
||||||
Some(rewritten.to_string())
|
Some(rewritten.to_string())
|
||||||
@@ -380,3 +409,52 @@ fn is_hop_by_hop_header(name: &HeaderName) -> bool {
|
|||||||
})
|
})
|
||||||
.contains(name.as_str())
|
.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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user