Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions crates/goose-server/src/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<TlsSetup> {
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).
Expand All @@ -115,6 +175,12 @@ pub async fn self_signed_config() -> Result<TlsSetup> {
#[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());
Expand All @@ -123,6 +189,9 @@ pub async fn self_signed_config() -> Result<TlsSetup> {
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(),
Expand Down
2 changes: 1 addition & 1 deletion ui/desktop/src/goosed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const findGoosedBinaryPath = (options: FindBinaryOptions = {}): string =>
};

export const checkServerStatus = async (client: Client, errorLog: string[]): Promise<boolean> => {
const timeout = 10000;
const timeout = 30000;
const interval = 100;
const maxAttempts = Math.ceil(timeout / interval);

Expand Down
Loading