From 8d56f26b351fbce53253745a10f218127f55dfe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Sat, 4 Apr 2026 14:35:59 +0200 Subject: [PATCH 1/2] fix(desktop): support self-signed remote HTTPS windows Route remote windows through a trusted local HTTPS proxy so WebView2 accepts secure cookies with self-signed servers. Install the rustls provider explicitly so the proxy starts reliably on Windows. --- packages/tauri-app/Cargo.lock | 545 +++++++++++++++++- packages/tauri-app/src-tauri/Cargo.toml | 10 +- .../tauri-app/src-tauri/src/cert_manager.rs | 159 +++++ packages/tauri-app/src-tauri/src/main.rs | 111 +++- .../tauri-app/src-tauri/src/remote_proxy.rs | 263 +++++++++ 5 files changed, 1063 insertions(+), 25 deletions(-) create mode 100644 packages/tauri-app/src-tauri/src/cert_manager.rs create mode 100644 packages/tauri-app/src-tauri/src/remote_proxy.rs diff --git a/packages/tauri-app/Cargo.lock b/packages/tauri-app/Cargo.lock index 2268df751..6e9405aab 100644 --- a/packages/tauri-app/Cargo.lock +++ b/packages/tauri-app/Cargo.lock @@ -47,6 +47,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -213,6 +222,105 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-server" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ab4a3ec9ea8a657c72d99a03a824af695bd0fb5ec639ccbd9cd3543b41a5f9" +dependencies = [ + "arc-swap", + "bytes", + "fs-err", + "http", + "http-body", + "hyper", + "hyper-util", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "base64" version = "0.21.7" @@ -408,6 +516,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -444,6 +554,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.44" @@ -456,17 +572,33 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "codenomad-tauri" version = "0.13.3" dependencies = [ "anyhow", + "axum", + "axum-server", + "bytes", "dirs 5.0.1", + "futures-util", "keepawake", "libc", "once_cell", "parking_lot", + "rcgen", "regex", + "reqwest 0.12.28", + "rustls", "serde", "serde_json", "serde_yaml", @@ -477,6 +609,7 @@ dependencies = [ "tauri-plugin-notification", "tauri-plugin-opener", "thiserror 1.0.69", + "tokio", "url", "which", "windows-sys 0.59.0", @@ -969,6 +1102,15 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.1" @@ -1139,6 +1281,22 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" +dependencies = [ + "autocfg", + "tokio", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futf" version = "0.1.5" @@ -1379,8 +1537,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1390,9 +1550,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1574,6 +1736,25 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1689,6 +1870,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.8.1" @@ -1699,9 +1886,11 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -1710,6 +1899,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1999,6 +2205,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.91" @@ -2157,6 +2373,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "mac" version = "0.1.1" @@ -2217,6 +2439,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" version = "2.8.0" @@ -2607,6 +2835,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2995,6 +3233,61 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.45" @@ -3132,6 +3425,19 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -3212,6 +3518,50 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", + "webpki-roots", +] + [[package]] name = "reqwest" version = "0.13.2" @@ -3242,7 +3592,7 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.5.0", "web-sys", ] @@ -3270,6 +3620,20 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -3311,6 +3675,53 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -3502,6 +3913,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -3531,6 +3953,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.18.0" @@ -3792,6 +4226,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3943,7 +4383,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "serde_repr", @@ -4367,6 +4807,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.50.0" @@ -4378,9 +4833,31 @@ dependencies = [ "mio", "pin-project-lite", "socket2", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -4512,6 +4989,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -4550,6 +5028,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -4691,6 +5170,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -4902,6 +5387,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasm-streams" version = "0.5.0" @@ -4937,6 +5435,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web_atoms" version = "0.2.3" @@ -4993,6 +5501,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -5286,6 +5803,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -5802,6 +6328,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.8.1" @@ -5927,6 +6462,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.3" diff --git a/packages/tauri-app/src-tauri/Cargo.toml b/packages/tauri-app/src-tauri/Cargo.toml index 13a89b1f7..a0021ff5e 100644 --- a/packages/tauri-app/src-tauri/Cargo.toml +++ b/packages/tauri-app/src-tauri/Cargo.toml @@ -12,11 +12,19 @@ tauri = { version = "2.5.2", features = [ "devtools"] } serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" +axum = "0.7" +axum-server = { version = "0.7", features = ["tls-rustls"] } +bytes = "1" +futures-util = "0.3" +rcgen = "0.13" +rustls = { version = "0.23", features = ["ring"] } +reqwest = { version = "0.12", default-features = false, features = ["http2", "charset", "json", "stream", "rustls-tls"] } regex = "1" once_cell = "1" parking_lot = "0.12" thiserror = "1" anyhow = "1" +tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "sync"] } which = "4" libc = "0.2" keepawake = "0.6" @@ -28,4 +36,4 @@ url = "2" tauri-plugin-notification = "2" [target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.59", features = ["Win32_UI_Shell"] } +windows-sys = { version = "0.59", features = ["Win32_UI_Shell", "Win32_Security_Cryptography"] } diff --git a/packages/tauri-app/src-tauri/src/cert_manager.rs b/packages/tauri-app/src-tauri/src/cert_manager.rs new file mode 100644 index 000000000..aed35282d --- /dev/null +++ b/packages/tauri-app/src-tauri/src/cert_manager.rs @@ -0,0 +1,159 @@ +use rcgen::{CertificateParams, DnType, KeyPair, SanType}; +use std::fs; +use std::path::PathBuf; +use std::time::Duration; + +const CERT_DIR_NAME: &str = "proxy-certs"; +const CERT_PEM_FILE: &str = "proxy.crt"; +const KEY_PEM_FILE: &str = "proxy.key"; +const CERT_DER_FILE: &str = "proxy.der"; +const TRUSTED_MARKER: &str = ".trusted"; + +/// Holds PEM-encoded certificate and private key for the local HTTPS proxy. +pub struct LocalCert { + pub cert_pem: String, + pub key_pem: String, + pub cert_der: Vec, +} + +/// Returns the directory where proxy certificates are stored. +fn cert_dir() -> Result { + let base = dirs::data_local_dir() + .ok_or_else(|| "Cannot determine local app data directory".to_string())?; + Ok(base + .join(crate::WINDOWS_APP_USER_MODEL_ID) + .join(CERT_DIR_NAME)) +} + +/// Ensures a self-signed certificate exists on disk, generating one if needed. +/// Returns the PEM cert, PEM key, and DER cert bytes. +pub fn ensure_local_cert() -> Result { + let dir = cert_dir()?; + let cert_pem_path = dir.join(CERT_PEM_FILE); + let key_pem_path = dir.join(KEY_PEM_FILE); + let cert_der_path = dir.join(CERT_DER_FILE); + + // If all files exist, load from disk + if cert_pem_path.exists() && key_pem_path.exists() && cert_der_path.exists() { + let cert_pem = fs::read_to_string(&cert_pem_path) + .map_err(|e| format!("Failed to read {}: {e}", cert_pem_path.display()))?; + let key_pem = fs::read_to_string(&key_pem_path) + .map_err(|e| format!("Failed to read {}: {e}", key_pem_path.display()))?; + let cert_der = fs::read(&cert_der_path) + .map_err(|e| format!("Failed to read {}: {e}", cert_der_path.display()))?; + return Ok(LocalCert { + cert_pem, + key_pem, + cert_der, + }); + } + + // Generate a new self-signed certificate + fs::create_dir_all(&dir) + .map_err(|e| format!("Failed to create cert dir {}: {e}", dir.display()))?; + + let mut params = CertificateParams::default(); + params + .distinguished_name + .push(DnType::CommonName, "CodeNomad Local Proxy"); + params.subject_alt_names = vec![ + SanType::DnsName("localhost".try_into().map_err(|e| format!("{e}"))?), + SanType::IpAddress(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)), + ]; + + // Valid for 10 years + params.not_before = rcgen::date_time_ymd(2024, 1, 1); + let ten_years = Duration::from_secs(10 * 365 * 24 * 3600); + params.not_after = params.not_before + ten_years; + + let key_pair = KeyPair::generate().map_err(|e| format!("Key generation failed: {e}"))?; + let cert = params + .self_signed(&key_pair) + .map_err(|e| format!("Certificate generation failed: {e}"))?; + + let cert_pem = cert.pem(); + let key_pem = key_pair.serialize_pem(); + let cert_der = cert.der().to_vec(); + + fs::write(&cert_pem_path, &cert_pem) + .map_err(|e| format!("Failed to write {}: {e}", cert_pem_path.display()))?; + fs::write(&key_pem_path, &key_pem) + .map_err(|e| format!("Failed to write {}: {e}", key_pem_path.display()))?; + fs::write(&cert_der_path, &cert_der) + .map_err(|e| format!("Failed to write {}: {e}", cert_der_path.display()))?; + + // Remove the trusted marker since this is a new cert + let marker = dir.join(TRUSTED_MARKER); + let _ = fs::remove_file(&marker); + + Ok(LocalCert { + cert_pem, + key_pem, + cert_der, + }) +} + +/// Returns true if the certificate has already been added to the Windows trust +/// store (indicated by the `.trusted` marker file). +pub fn is_cert_trusted() -> bool { + cert_dir() + .map(|dir| dir.join(TRUSTED_MARKER).exists()) + .unwrap_or(false) +} + +/// Adds the DER-encoded certificate to the Windows `CurrentUser\Root` store. +/// This will show a one-time Windows security confirmation dialog. +/// After success, writes a `.trusted` marker file to avoid re-prompting. +#[cfg(windows)] +pub fn trust_cert_in_store(cert_der: &[u8]) -> Result<(), String> { + use windows_sys::Win32::Security::Cryptography::{ + CertAddEncodedCertificateToStore, CertCloseStore, CertOpenSystemStoreW, + CERT_STORE_ADD_REPLACE_EXISTING, PKCS_7_ASN_ENCODING, X509_ASN_ENCODING, + }; + + if is_cert_trusted() { + return Ok(()); + } + + // "Root" in UTF-16 + let store_name: Vec = "Root\0".encode_utf16().collect(); + + unsafe { + let store = CertOpenSystemStoreW(0, store_name.as_ptr()); + if store.is_null() { + return Err("Failed to open CurrentUser\\Root certificate store".into()); + } + + let encoding = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING; + let result = CertAddEncodedCertificateToStore( + store, + encoding, + cert_der.as_ptr(), + cert_der.len() as u32, + CERT_STORE_ADD_REPLACE_EXISTING, + std::ptr::null_mut(), + ); + + CertCloseStore(store, 0); + + if result == 0 { + return Err("Failed to add certificate to trust store. \ + The user may have declined the security dialog." + .into()); + } + } + + // Write marker file + let dir = cert_dir()?; + fs::write(dir.join(TRUSTED_MARKER), "trusted") + .map_err(|e| format!("Failed to write trust marker: {e}"))?; + + Ok(()) +} + +#[cfg(not(windows))] +pub fn trust_cert_in_store(_cert_der: &[u8]) -> Result<(), String> { + // On non-Windows platforms, certificate trust is not yet implemented. + // The proxy will still work but the browser may show a warning. + Ok(()) +} diff --git a/packages/tauri-app/src-tauri/src/main.rs b/packages/tauri-app/src-tauri/src/main.rs index 1bbc83ef4..2738308f9 100644 --- a/packages/tauri-app/src-tauri/src/main.rs +++ b/packages/tauri-app/src-tauri/src/main.rs @@ -1,9 +1,12 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +mod cert_manager; mod cli_manager; +mod remote_proxy; use cli_manager::{CliProcessManager, CliStatus}; use keepawake::KeepAwake; +use remote_proxy::{start_remote_proxy, ProxyTlsConfig, RemoteProxyHandle}; use serde::Deserialize; use serde_json::json; use std::collections::HashMap; @@ -13,7 +16,9 @@ use std::time::{SystemTime, UNIX_EPOCH}; use tauri::menu::{MenuBuilder, MenuItem, SubmenuBuilder}; use tauri::plugin::{Builder as PluginBuilder, TauriPlugin}; use tauri::webview::Webview; -use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewUrl, WebviewWindowBuilder, WindowEvent, Wry}; +use tauri::{ + AppHandle, Emitter, Manager, Runtime, WebviewUrl, WebviewWindowBuilder, WindowEvent, Wry, +}; use tauri_plugin_global_shortcut::{ Code as ShortcutCode, GlobalShortcutExt, Shortcut, ShortcutState, }; @@ -43,6 +48,7 @@ pub struct AppState { pub wake_lock: Mutex>, pub zoom_level: Mutex, pub remote_origins: Mutex>, + pub remote_proxies: Mutex>, } #[derive(Debug, Deserialize)] @@ -117,7 +123,7 @@ fn is_dev_mode() -> bool { fn should_allow_internal(url: &Url) -> bool { match url.scheme() { - "tauri" | "asset" | "file" => true, + "tauri" | "asset" | "file" | "about" => true, // On Windows/WebView2, Tauri serves the app assets from `tauri.localhost`. // This must be treated as an internal origin or the navigation guard will // redirect it to the system browser and the app will appear blank. @@ -129,7 +135,11 @@ fn should_allow_internal(url: &Url) -> bool { } } -fn should_allow_window_origin(app_handle: &AppHandle, window_label: &str, url: &Url) -> bool { +fn should_allow_window_origin( + app_handle: &AppHandle, + window_label: &str, + url: &Url, +) -> bool { if should_allow_internal(url) { return true; } @@ -161,21 +171,54 @@ fn intercept_navigation(webview: &Webview, url: &Url) -> bool { false } -#[tauri::command] -fn open_remote_window(app: AppHandle, payload: RemoteWindowPayload) -> Result<(), String> { - if payload.skip_tls_verify && payload.base_url.starts_with("https://") { - return Err( - "Tauri cannot bypass self-signed HTTPS certificates automatically yet. Trust the certificate in your OS first, then reconnect, or use the CodeNomad Electron app." - .to_string(), - ); - } - +async fn open_remote_window_impl( + app: AppHandle, + payload: RemoteWindowPayload, + tls_config: Option, +) -> Result<(), String> { let parsed = Url::parse(&payload.base_url).map_err(|err| err.to_string())?; let label = format!("remote-{}", payload.id); - let title = format!("{} - {}", payload.name, parsed.host_str().unwrap_or(payload.base_url.as_str())); + let title = format!( + "{} - {}", + payload.name, + parsed.host_str().unwrap_or(payload.base_url.as_str()) + ); + + let state = app.state::(); + let reuses_existing_proxy = { + let proxies = state.remote_proxies.lock().map_err(|err| err.to_string())?; + proxies + .get(&label) + .map(|existing| existing.matches(&parsed, payload.skip_tls_verify)) + .unwrap_or(false) + }; + + let local_url = if reuses_existing_proxy { + let proxies = state.remote_proxies.lock().map_err(|err| err.to_string())?; + proxies + .get(&label) + .map(|handle| handle.local_base_url().clone()) + .ok_or_else(|| "Remote proxy disappeared before reuse".to_string())? + } else { + let new_proxy = + start_remote_proxy(parsed.clone(), payload.skip_tls_verify, tls_config).await?; + let local_url = new_proxy.local_base_url().clone(); + let mut proxies = state.remote_proxies.lock().map_err(|err| err.to_string())?; + if let Some(existing) = proxies.remove(&label) { + existing.shutdown(); + } + proxies.insert(label.clone(), new_proxy); + local_url + }; + + app.state::() + .remote_origins + .lock() + .map_err(|err| err.to_string())? + .insert(label.clone(), local_url.origin().ascii_serialization()); if let Some(existing) = app.get_webview_window(&label) { - let _ = existing.navigate(parsed.clone()); + let _ = existing.navigate(local_url.clone()); let _ = existing.set_title(&title); let _ = existing.show(); let _ = existing.unminimize(); @@ -183,13 +226,7 @@ fn open_remote_window(app: AppHandle, payload: RemoteWindowPayload) -> Result<() return Ok(()); } - app.state::() - .remote_origins - .lock() - .map_err(|err| err.to_string())? - .insert(label.clone(), parsed.origin().ascii_serialization()); - - let window = WebviewWindowBuilder::new(&app, label.clone(), WebviewUrl::External(parsed.clone())) + let window = WebviewWindowBuilder::new(&app, label.clone(), WebviewUrl::External(local_url)) .title(title) .inner_size(1400.0, 900.0) .min_inner_size(800.0, 600.0) @@ -197,10 +234,16 @@ fn open_remote_window(app: AppHandle, payload: RemoteWindowPayload) -> Result<() .map_err(|err| err.to_string())?; let app_handle = app.clone(); + let label_for_cleanup = label.clone(); window.on_window_event(move |event| { if let WindowEvent::Destroyed = event { if let Ok(mut origins) = app_handle.state::().remote_origins.lock() { - origins.remove(&label); + origins.remove(&label_for_cleanup); + } + if let Ok(mut proxies) = app_handle.state::().remote_proxies.lock() { + if let Some(handle) = proxies.remove(&label_for_cleanup) { + handle.shutdown(); + } } } }); @@ -208,6 +251,27 @@ fn open_remote_window(app: AppHandle, payload: RemoteWindowPayload) -> Result<() Ok(()) } +#[tauri::command] +async fn open_remote_window(app: AppHandle, payload: RemoteWindowPayload) -> Result<(), String> { + 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}"); + } + Some(ProxyTlsConfig { + cert_pem: local_cert.cert_pem, + key_pem: local_cert.key_pem, + }) + } + Err(err) => { + eprintln!("[tauri] failed to generate proxy cert, falling back to HTTP: {err}"); + None + } + }; + + open_remote_window_impl(app, payload, tls_config).await +} + fn collect_directory_paths(paths: &[std::path::PathBuf]) -> Vec { paths .iter() @@ -335,6 +399,8 @@ fn set_windows_app_user_model_id() { fn set_windows_app_user_model_id() {} fn main() { + let _ = rustls::crypto::ring::default_provider().install_default(); + let navigation_guard: TauriPlugin = PluginBuilder::new("external-link-guard") .on_navigation(|webview, url| intercept_navigation(webview, url)) .build(); @@ -362,6 +428,7 @@ fn main() { wake_lock: Mutex::new(None), zoom_level: Mutex::new(DEFAULT_ZOOM_LEVEL), remote_origins: Mutex::new(HashMap::new()), + remote_proxies: Mutex::new(HashMap::new()), }) .setup(|app| { set_windows_app_user_model_id(); diff --git a/packages/tauri-app/src-tauri/src/remote_proxy.rs b/packages/tauri-app/src-tauri/src/remote_proxy.rs new file mode 100644 index 000000000..9a8978986 --- /dev/null +++ b/packages/tauri-app/src-tauri/src/remote_proxy.rs @@ -0,0 +1,263 @@ +use axum::body::Body; +use axum::extract::{Request, State}; +use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode, Uri}; +use axum::response::Response; +use axum::routing::any; +use axum::Router; +use axum_server::tls_rustls::RustlsConfig; +use futures_util::TryStreamExt; +use reqwest::redirect::Policy; +use reqwest::Client; +use std::collections::HashSet; +use std::sync::Arc; +use url::Url; + +#[derive(Clone)] +struct ProxyState { + client: Client, + target_base_url: Url, + local_base_url: Url, +} + +/// TLS configuration for the local HTTPS proxy. +pub struct ProxyTlsConfig { + pub cert_pem: String, + pub key_pem: String, +} + +pub struct RemoteProxyHandle { + local_base_url: Url, + target_base_url: Url, + skip_tls_verify: bool, + server_handle: axum_server::Handle, +} + +impl RemoteProxyHandle { + pub fn local_base_url(&self) -> &Url { + &self.local_base_url + } + + pub fn matches(&self, target_base_url: &Url, skip_tls_verify: bool) -> bool { + self.target_base_url == *target_base_url && self.skip_tls_verify == skip_tls_verify + } + + pub fn shutdown(&self) { + self.server_handle.shutdown(); + } +} + +impl Drop for RemoteProxyHandle { + fn drop(&mut self) { + self.shutdown(); + } +} + +pub async fn start_remote_proxy( + target_base_url: Url, + skip_tls_verify: bool, + tls_config: Option, +) -> Result { + let client = Client::builder() + .redirect(Policy::none()) + .danger_accept_invalid_certs(skip_tls_verify) + .build() + .map_err(|err| err.to_string())?; + + // Pre-bind a std TcpListener on port 0 to discover the actual port + let std_listener = std::net::TcpListener::bind("127.0.0.1:0") + .map_err(|err| err.to_string())?; + let address = std_listener.local_addr().map_err(|err| err.to_string())?; + + let scheme = if tls_config.is_some() { "https" } else { "http" }; + let local_base_url = + Url::parse(&format!("{scheme}://{address}")).map_err(|err| err.to_string())?; + + let state = Arc::new(ProxyState { + client, + target_base_url: target_base_url.clone(), + local_base_url: local_base_url.clone(), + }); + + let app = Router::new() + .route("/*path", any(proxy_request)) + .route("/", any(proxy_request)) + .with_state(state); + + let server_handle = axum_server::Handle::new(); + let handle_clone = server_handle.clone(); + + if let Some(tls) = tls_config { + let rustls_config = + RustlsConfig::from_pem(tls.cert_pem.into_bytes(), tls.key_pem.into_bytes()) + .await + .map_err(|err| format!("Failed to build RustlsConfig: {err}"))?; + + tauri::async_runtime::spawn(async move { + let server = axum_server::from_tcp_rustls(std_listener, rustls_config) + .handle(handle_clone) + .serve(app.into_make_service()); + + if let Err(err) = server.await { + eprintln!("[tauri] remote proxy (HTTPS) stopped with error: {err}"); + } + }); + } else { + tauri::async_runtime::spawn(async move { + let server = axum_server::from_tcp(std_listener) + .handle(handle_clone) + .serve(app.into_make_service()); + + if let Err(err) = server.await { + eprintln!("[tauri] remote proxy (HTTP) stopped with error: {err}"); + } + }); + } + + Ok(RemoteProxyHandle { + local_base_url, + target_base_url, + skip_tls_verify, + server_handle, + }) +} + +async fn proxy_request( + State(state): State>, + request: Request, +) -> Result, StatusCode> { + let upstream_url = build_upstream_url(&state.target_base_url, request.uri()) + .map_err(|_| StatusCode::BAD_REQUEST)?; + + let mut builder = state + .client + .request(request.method().clone(), upstream_url.clone()); + builder = builder.headers(filter_request_headers( + request.headers(), + &state.target_base_url, + )?); + + let body = axum::body::to_bytes(request.into_body(), usize::MAX) + .await + .map_err(|_| StatusCode::BAD_REQUEST)?; + if !body.is_empty() { + builder = builder.body(body); + } + + let upstream = builder.send().await.map_err(map_upstream_error)?; + let status = upstream.status(); + let headers = rewrite_response_headers( + upstream.headers(), + &state.target_base_url, + &state.local_base_url, + )?; + let stream = upstream + .bytes_stream() + .map_err(|err| std::io::Error::other(err.to_string())); + + let mut response = Response::new(Body::from_stream(stream)); + *response.status_mut() = status; + *response.headers_mut() = headers; + Ok(response) +} + +fn build_upstream_url(base_url: &Url, uri: &Uri) -> Result { + let mut url = base_url.join(uri.path().trim_start_matches('/'))?; + url.set_query(uri.query()); + Ok(url) +} + +fn filter_request_headers( + headers: &HeaderMap, + target_base_url: &Url, +) -> Result { + let mut forwarded = HeaderMap::new(); + for (name, value) in headers { + if is_hop_by_hop_header(name) || *name == axum::http::header::HOST { + continue; + } + forwarded.append(name.clone(), value.clone()); + } + + let host = target_base_url.host_str().ok_or(StatusCode::BAD_REQUEST)?; + let host_value = match target_base_url.port() { + Some(port) => format!("{host}:{port}"), + None => host.to_string(), + }; + forwarded.insert( + axum::http::header::HOST, + HeaderValue::from_str(&host_value).map_err(|_| StatusCode::BAD_REQUEST)?, + ); + + Ok(forwarded) +} + +fn rewrite_response_headers( + headers: &HeaderMap, + target_base_url: &Url, + local_base_url: &Url, +) -> Result { + let mut rewritten = HeaderMap::new(); + for (name, value) in headers { + if is_hop_by_hop_header(name) { + continue; + } + + if *name == axum::http::header::LOCATION { + if let Ok(location) = value.to_str() { + let next = rewrite_location(location, target_base_url, local_base_url); + rewritten.append( + name.clone(), + HeaderValue::from_str(&next).map_err(|_| StatusCode::BAD_GATEWAY)?, + ); + continue; + } + } + + rewritten.append(name.clone(), value.clone()); + } + Ok(rewritten) +} + +fn rewrite_location(location: &str, target_base_url: &Url, local_base_url: &Url) -> String { + let Ok(parsed) = target_base_url.join(location) else { + return location.to_string(); + }; + + if parsed.origin() != target_base_url.origin() { + return location.to_string(); + } + + let mut rewritten = local_base_url.clone(); + rewritten.set_path(parsed.path()); + rewritten.set_query(parsed.query()); + rewritten.set_fragment(parsed.fragment()); + rewritten.to_string() +} + +fn map_upstream_error(error: reqwest::Error) -> StatusCode { + if error.is_timeout() { + StatusCode::GATEWAY_TIMEOUT + } else if error.is_connect() { + StatusCode::BAD_GATEWAY + } else { + StatusCode::INTERNAL_SERVER_ERROR + } +} + +fn is_hop_by_hop_header(name: &HeaderName) -> bool { + static HOP_BY_HOP: std::sync::OnceLock> = std::sync::OnceLock::new(); + HOP_BY_HOP + .get_or_init(|| { + HashSet::from([ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", + ]) + }) + .contains(name.as_str()) +} From ba9cd5304aea2dc2b34db8eadc486f28458fae50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Sat, 4 Apr 2026 17:37:31 +0200 Subject: [PATCH 2/2] fix(desktop): harden remote proxy access Bootstrap each remote window proxy with a random one-time token, establish a local secure session cookie, and reject unauthenticated loopback requests so other local processes cannot casually reuse the proxy port. --- packages/tauri-app/Cargo.lock | 1 + packages/tauri-app/src-tauri/Cargo.toml | 1 + packages/tauri-app/src-tauri/src/main.rs | 4 +- .../tauri-app/src-tauri/src/remote_proxy.rs | 92 ++++++++++++++++++- 4 files changed, 95 insertions(+), 3 deletions(-) diff --git a/packages/tauri-app/Cargo.lock b/packages/tauri-app/Cargo.lock index 6e9405aab..2aa88ee3d 100644 --- a/packages/tauri-app/Cargo.lock +++ b/packages/tauri-app/Cargo.lock @@ -595,6 +595,7 @@ dependencies = [ "libc", "once_cell", "parking_lot", + "rand 0.8.5", "rcgen", "regex", "reqwest 0.12.28", diff --git a/packages/tauri-app/src-tauri/Cargo.toml b/packages/tauri-app/src-tauri/Cargo.toml index a0021ff5e..9ce43484f 100644 --- a/packages/tauri-app/src-tauri/Cargo.toml +++ b/packages/tauri-app/src-tauri/Cargo.toml @@ -19,6 +19,7 @@ futures-util = "0.3" rcgen = "0.13" rustls = { version = "0.23", features = ["ring"] } reqwest = { version = "0.12", default-features = false, features = ["http2", "charset", "json", "stream", "rustls-tls"] } +rand = "0.8" regex = "1" once_cell = "1" parking_lot = "0.12" diff --git a/packages/tauri-app/src-tauri/src/main.rs b/packages/tauri-app/src-tauri/src/main.rs index 2738308f9..b8029174a 100644 --- a/packages/tauri-app/src-tauri/src/main.rs +++ b/packages/tauri-app/src-tauri/src/main.rs @@ -197,12 +197,12 @@ async fn open_remote_window_impl( let proxies = state.remote_proxies.lock().map_err(|err| err.to_string())?; proxies .get(&label) - .map(|handle| handle.local_base_url().clone()) + .map(|handle| handle.entry_url().clone()) .ok_or_else(|| "Remote proxy disappeared before reuse".to_string())? } else { let new_proxy = start_remote_proxy(parsed.clone(), payload.skip_tls_verify, tls_config).await?; - let local_url = new_proxy.local_base_url().clone(); + let local_url = new_proxy.entry_url().clone(); let mut proxies = state.remote_proxies.lock().map_err(|err| err.to_string())?; if let Some(existing) = proxies.remove(&label) { existing.shutdown(); diff --git a/packages/tauri-app/src-tauri/src/remote_proxy.rs b/packages/tauri-app/src-tauri/src/remote_proxy.rs index 9a8978986..2534cdd9e 100644 --- a/packages/tauri-app/src-tauri/src/remote_proxy.rs +++ b/packages/tauri-app/src-tauri/src/remote_proxy.rs @@ -6,17 +6,22 @@ use axum::routing::any; use axum::Router; use axum_server::tls_rustls::RustlsConfig; use futures_util::TryStreamExt; +use rand::RngCore; use reqwest::redirect::Policy; use reqwest::Client; use std::collections::HashSet; use std::sync::Arc; use url::Url; +const PROXY_COOKIE_NAME: &str = "codenomad_remote_proxy"; +const PROXY_TOKEN_QUERY: &str = "proxy_token"; + #[derive(Clone)] struct ProxyState { client: Client, target_base_url: Url, local_base_url: Url, + session_token: String, } /// TLS configuration for the local HTTPS proxy. @@ -27,6 +32,7 @@ pub struct ProxyTlsConfig { pub struct RemoteProxyHandle { local_base_url: Url, + entry_url: Url, target_base_url: Url, skip_tls_verify: bool, server_handle: axum_server::Handle, @@ -37,6 +43,10 @@ impl RemoteProxyHandle { &self.local_base_url } + pub fn entry_url(&self) -> &Url { + &self.entry_url + } + pub fn matches(&self, target_base_url: &Url, skip_tls_verify: bool) -> bool { self.target_base_url == *target_base_url && self.skip_tls_verify == skip_tls_verify } @@ -71,11 +81,15 @@ pub async fn start_remote_proxy( let scheme = if tls_config.is_some() { "https" } else { "http" }; let local_base_url = Url::parse(&format!("{scheme}://{address}")).map_err(|err| err.to_string())?; + let session_token = generate_session_token(); + let mut entry_url = local_base_url.clone(); + entry_url.set_query(Some(&format!("{PROXY_TOKEN_QUERY}={session_token}"))); let state = Arc::new(ProxyState { client, target_base_url: target_base_url.clone(), local_base_url: local_base_url.clone(), + session_token, }); let app = Router::new() @@ -115,6 +129,7 @@ pub async fn start_remote_proxy( Ok(RemoteProxyHandle { local_base_url, + entry_url, target_base_url, skip_tls_verify, server_handle, @@ -125,6 +140,13 @@ async fn proxy_request( State(state): State>, request: Request, ) -> Result, StatusCode> { + if !request_is_authorized(&request, &state.session_token) { + if request_bootstraps_session(&request, &state.session_token) { + return Ok(build_bootstrap_response(request.uri(), &state.session_token)?); + } + return Err(StatusCode::FORBIDDEN); + } + let upstream_url = build_upstream_url(&state.target_base_url, request.uri()) .map_err(|_| StatusCode::BAD_REQUEST)?; @@ -162,10 +184,78 @@ async fn proxy_request( fn build_upstream_url(base_url: &Url, uri: &Uri) -> Result { let mut url = base_url.join(uri.path().trim_start_matches('/'))?; - url.set_query(uri.query()); + url.set_query(strip_proxy_token_query(uri.query()).as_deref()); Ok(url) } +fn generate_session_token() -> String { + let mut bytes = [0_u8; 16]; + rand::thread_rng().fill_bytes(&mut bytes); + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + +fn request_is_authorized(request: &Request, session_token: &str) -> bool { + request + .headers() + .get(axum::http::header::COOKIE) + .and_then(|value| value.to_str().ok()) + .map(|cookie_header| { + cookie_header + .split(';') + .map(str::trim) + .filter_map(|part| part.split_once('=')) + .any(|(name, value)| name == PROXY_COOKIE_NAME && value == session_token) + }) + .unwrap_or(false) +} + +fn request_bootstraps_session(request: &Request, session_token: &str) -> bool { + request.uri().query().is_some_and(|query| { + url::form_urlencoded::parse(query.as_bytes()) + .any(|(name, value)| name == PROXY_TOKEN_QUERY && value == session_token) + }) +} + +fn build_bootstrap_response(uri: &Uri, session_token: &str) -> Result, StatusCode> { + let redirect_target = sanitized_request_target(uri); + let cookie = format!( + "{PROXY_COOKIE_NAME}={session_token}; Path=/; HttpOnly; SameSite=Strict; Secure" + ); + + Response::builder() + .status(StatusCode::FOUND) + .header(axum::http::header::LOCATION, redirect_target) + .header(axum::http::header::SET_COOKIE, cookie) + .body(Body::empty()) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR) +} + +fn sanitized_request_target(uri: &Uri) -> String { + let path = if uri.path().is_empty() { "/" } else { uri.path() }; + match strip_proxy_token_query(uri.query()) { + Some(query) if !query.is_empty() => format!("{path}?{query}"), + _ => path.to_string(), + } +} + +fn strip_proxy_token_query(query: Option<&str>) -> Option { + let query = query?; + let filtered: Vec<(std::borrow::Cow<'_, str>, std::borrow::Cow<'_, str>)> = + url::form_urlencoded::parse(query.as_bytes()) + .filter(|(name, _)| name != PROXY_TOKEN_QUERY) + .collect(); + + if filtered.is_empty() { + return None; + } + + Some( + url::form_urlencoded::Serializer::new(String::new()) + .extend_pairs(filtered) + .finish(), + ) +} + fn filter_request_headers( headers: &HeaderMap, target_base_url: &Url,