From 092a40c44b7fb56719a996af1ef9e7cd40aa7e05 Mon Sep 17 00:00:00 2001 From: Treebird Date: Thu, 23 Apr 2026 03:16:02 +0300 Subject: [PATCH] fix(goose-server): cache TLS cert to disk to avoid slow startup on first launch (#8174) Signed-off-by: Treebird Co-authored-by: Claude Sonnet 4.6 --- crates/goose-server/src/tls.rs | 69 ++++++++++++++++++++++++++++++++++ ui/desktop/src/goosed.ts | 2 +- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/crates/goose-server/src/tls.rs b/crates/goose-server/src/tls.rs index 8f1431a2..273052ec 100644 --- a/crates/goose-server/src/tls.rs +++ b/crates/goose-server/src/tls.rs @@ -12,6 +12,7 @@ //! the server listener always uses OpenSSL when this feature is active. use anyhow::{bail, Result}; +use goose::config::paths::Paths; use rcgen::{CertificateParams, DnType, KeyPair, SanType}; use std::path::Path; @@ -105,6 +106,65 @@ pub async fn setup_tls(cert_path: Option<&str>, key_path: Option<&str>) -> Resul } } +fn tls_cache_dir() -> std::path::PathBuf { + Paths::config_dir().join("tls") +} + +fn write_private_key(path: &std::path::Path, contents: &[u8]) { + #[cfg(unix)] + { + use std::io::Write; + use std::os::unix::fs::OpenOptionsExt; + + let result = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o600) + .open(path); + if let Ok(mut file) = result { + let _ = file.write_all(contents); + } + } + + #[cfg(not(unix))] + { + let _ = std::fs::write(path, contents); + } +} + +async fn load_cached_tls() -> Option { + let dir = tls_cache_dir(); + let cert_pem = std::fs::read(dir.join("server.pem")).ok()?; + let key_pem = std::fs::read(dir.join("server.key")).ok()?; + + let der = pem::parse(&cert_pem).ok()?.into_contents(); + let fingerprint = sha256_fingerprint(&der); + + #[cfg(feature = "rustls-tls")] + let config = axum_server::tls_rustls::RustlsConfig::from_pem(cert_pem, key_pem) + .await + .ok()?; + #[cfg(feature = "native-tls")] + let config = axum_server::tls_openssl::OpenSSLConfig::from_pem(&cert_pem, &key_pem).ok()?; + + Some(TlsSetup { + config, + fingerprint, + }) +} + +/// All errors are silently ignored — this is a best-effort optimisation and +/// must never prevent the server from starting. +fn save_tls_to_cache(cert_pem: &str, key_pem: &str) { + let dir = tls_cache_dir(); + if std::fs::create_dir_all(&dir).is_err() { + return; + } + let _ = std::fs::write(dir.join("server.pem"), cert_pem); + write_private_key(&dir.join("server.key"), key_pem.as_bytes()); +} + /// Generate a self-signed TLS certificate for localhost (127.0.0.1) and /// return a [`TlsSetup`] containing the server config and the SHA-256 /// fingerprint of the generated certificate (colon-separated hex). @@ -115,6 +175,12 @@ pub async fn self_signed_config() -> Result { #[cfg(feature = "rustls-tls")] let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + // Fast path: reuse a previously cached certificate if one exists. + if let Some(cached) = load_cached_tls().await { + println!("GOOSED_CERT_FINGERPRINT={}", cached.fingerprint); + return Ok(cached); + } + let (cert, key_pair) = generate_self_signed_cert()?; let fingerprint = sha256_fingerprint(cert.der()); @@ -123,6 +189,9 @@ pub async fn self_signed_config() -> Result { let cert_pem = cert.pem(); let key_pem = key_pair.serialize_pem(); + // Persist for future restarts before moving the strings into the config. + save_tls_to_cache(&cert_pem, &key_pem); + #[cfg(feature = "rustls-tls")] let config = axum_server::tls_rustls::RustlsConfig::from_pem( cert_pem.into_bytes(), diff --git a/ui/desktop/src/goosed.ts b/ui/desktop/src/goosed.ts index 0dafe833..cf74bb31 100644 --- a/ui/desktop/src/goosed.ts +++ b/ui/desktop/src/goosed.ts @@ -78,7 +78,7 @@ export const findGoosedBinaryPath = (options: FindBinaryOptions = {}): string => }; export const checkServerStatus = async (client: Client, errorLog: string[]): Promise => { - const timeout = 10000; + const timeout = 30000; const interval = 100; const maxAttempts = Math.ceil(timeout / interval);