diff --git a/Cargo.lock b/Cargo.lock index 2726f3d..babe768 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,7 @@ dependencies = [ "actix-codec", "actix-rt", "actix-service", + "actix-tls", "actix-utils", "ahash 0.8.7", "base64", @@ -160,6 +161,7 @@ dependencies = [ "actix-rt", "actix-server", "actix-service", + "actix-tls", "actix-utils", "ahash 0.8.7", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 4ed53b1..7a0a2fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ default = [] [dependencies] actix-rt = "2.7.0" -actix-web = { version = "4.0.0", default-features = false } +actix-web = { version = "4.0.0", default-features = false, features = ["rustls-0_21"] } anyhow = "1.0" awc = { version = "3.0.0", default-features = false, features = ["rustls-0_21"] } clap = { version = "4.0.2", features = ["derive", "env"] } diff --git a/src/main.rs b/src/main.rs index fee6be5..45516f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ use actix_web::{ middleware::NormalizePath, web, App, HttpRequest, HttpResponse, HttpResponseBuilder, HttpServer, ResponseError, }; +use anyhow::Context; use awc::{Client, Connector}; use clap::Parser; use console_subscriber::ConsoleLayer; @@ -17,7 +18,10 @@ use once_cell::sync::Lazy; use opentelemetry::KeyValue; use opentelemetry_otlp::WithExportConfig; use opentelemetry_sdk::{propagation::TraceContextPropagator, Resource}; -use rustls::{Certificate, ClientConfig, OwnedTrustAnchor, RootCertStore}; +use rustls::{ + sign::CertifiedKey, Certificate, ClientConfig, OwnedTrustAnchor, PrivateKey, RootCertStore, + ServerConfig, +}; use std::{ io::Cursor, net::SocketAddr, @@ -96,9 +100,23 @@ struct Config { short, long, env = "PICTRS_PROXY_CERTIFICATE", - help = "Path to the certificate file to connec to pict-rs over TLS" + help = "Path to the certificate file to connect to pict-rs over TLS" )] certificate: Option, + + #[arg( + long, + env = "PICTRS_PROXY_SERVER_CERTIFICATE", + help = "Path to the certificate file to serve pict-rs-proxy over TLS" + )] + server_certificate: Option, + + #[arg( + long, + env = "PICTRS_PROXY_SERVER_PRIVATE_KEY", + help = "Path to the private key file to serve pict-rs-proxy over TLS" + )] + server_private_key: Option, } impl Config { @@ -880,7 +898,7 @@ fn init_tracing( let subscriber = subscriber.with(console_layer); init_subscriber(subscriber, targets, opentelemetry_url, service_name)?; - tracing::info!("Launched console on {console_addr}"); + tracing::info!("Serving tokio-console endpoint on {console_addr}"); } else { init_subscriber(subscriber, targets, opentelemetry_url, service_name)?; } @@ -927,7 +945,7 @@ where Ok(()) } -async fn create_rustls_config() -> anyhow::Result { +async fn rustls_client_config() -> anyhow::Result { let mut cert_store = RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS .into_iter() @@ -955,6 +973,37 @@ async fn create_rustls_config() -> anyhow::Result { .with_no_client_auth()) } +async fn rustls_server_key() -> anyhow::Result> { + let certificate_path = if let Some(c) = &CONFIG.server_certificate { + c + } else { + tracing::info!("No server certificate"); + return Ok(None); + }; + + let private_key_path = if let Some(p) = &CONFIG.server_private_key { + p + } else { + tracing::info!("No server private_key"); + return Ok(None); + }; + + let cert_bytes = tokio::fs::read(certificate_path).await?; + + let certs = rustls_pemfile::certs(&mut cert_bytes.as_slice()) + .map(|res| res.map(|c| Certificate(c.to_vec()))) + .collect::, _>>()?; + + let key_bytes = tokio::fs::read(private_key_path).await?; + + let key = + rustls_pemfile::private_key(&mut key_bytes.as_slice())?.context("No key in keyfile")?; + + let signing_key = rustls::sign::any_supported_type(&PrivateKey(Vec::from(key.secret_der())))?; + + Ok(Some(CertifiedKey::new(certs, signing_key))) +} + #[actix_rt::main] async fn main() -> anyhow::Result<()> { dotenv::dotenv().ok(); @@ -966,9 +1015,9 @@ async fn main() -> anyhow::Result<()> { CONFIG.console_event_buffer_size, )?; - let client_config = create_rustls_config().await?; + let client_config = rustls_client_config().await?; - HttpServer::new(move || { + let server = HttpServer::new(move || { let client = Client::builder() .wrap(Tracing) .add_default_header(("User-Agent", "pict-rs-frontend, v0.5.0")) @@ -999,10 +1048,45 @@ async fn main() -> anyhow::Result<()> { .service(web::resource("/delete").route(web::get().to(delete))) .service(web::resource("/404").route(web::get().to(not_found))) .default_service(web::get().to(go_home)) - }) - .bind(CONFIG.addr)? - .run() - .await?; + }); + + if let Some(key) = rustls_server_key().await? { + tracing::info!("Serving pict-rs-proxy over TLS on {}", CONFIG.addr); + + let (tx, rx) = rustls_channel_resolver::channel::<32>(key); + + let handle = actix_rt::spawn(async move { + let mut interval = actix_rt::time::interval(Duration::from_secs(30)); + interval.tick().await; + + loop { + interval.tick().await; + + match rustls_server_key().await { + Ok(Some(key)) => tx.update(key), + Ok(None) => tracing::warn!("Missing server certificates"), + Err(e) => tracing::error!("Failed to read server certificates {e}"), + } + } + }); + + let server_config = ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_cert_resolver(rx); + + server + .bind_rustls_021(CONFIG.addr, server_config)? + .run() + .await?; + + handle.abort(); + let _ = handle.await; + } else { + tracing::info!("Serving pict-rs-proxy on {}", CONFIG.addr); + + server.bind(CONFIG.addr)?.run().await?; + } Ok(()) }