use crate::daemon::{Daemon, DaemonError, DaemonManager}; use crate::tlsgen::{Tls, TlsError}; use log::debug; use rand::random; use serde::Serialize; use std::ffi::OsStr; use std::ops::Range; use std::path::{Path, PathBuf}; use std::time::Instant; use tempfile::{tempdir, TempDir}; const PORT_RANGE: Range = 2048..32000; const TIMEOUT_MS: u128 = 10_000; type PortNumber = u16; #[derive(Debug, thiserror::Error)] pub enum ObnamServerError { #[error("took too long to pick a random port for server")] Port, #[error("error creating chunks directory: {0}")] ChunksDir(std::io::Error), #[error("failed to create temporary directory for server: {0}")] TempDir(std::io::Error), #[error("failed to write server configuration to {0}: {1}")] WriteConfig(PathBuf, std::io::Error), #[error("failed to create TLS certificate: {0}")] Tls(TlsError), #[error("failed to write TLS certificate: {0}")] WriteTls(PathBuf, std::io::Error), #[error("failed to start Obnam server: {0}")] Daemon(DaemonError), } #[derive(Debug)] pub struct ObnamServer { #[allow(dead_code)] tempdir: TempDir, chunks: PathBuf, daemon: Option, url: String, } impl ObnamServer { pub fn new(server_binary: &Path, manager: &DaemonManager) -> Result { debug!("creating ObnamServer"); let tempdir = tempdir().map_err(ObnamServerError::TempDir)?; let config_filename = tempdir.path().join("server.yaml"); let chunks = tempdir.path().join("chunks"); let tls_key = tempdir.path().join("tls_key"); let tls_cert = tempdir.path().join("tls_cert"); let tls = Tls::new().map_err(ObnamServerError::Tls)?; write(&tls_key, tls.key())?; write(&tls_cert, tls.cert())?; std::fs::create_dir(&chunks).map_err(ObnamServerError::ChunksDir)?; let port = pick_port()?; debug!("server listens on port {}", port); let config = ServerConfig::new(port, chunks.clone(), tls_key, tls_cert); config.write(&config_filename)?; let daemon = manager .start( &[OsStr::new(server_binary), OsStr::new(&config_filename)], Path::new("server.out"), Path::new("server.log"), ) .map_err(ObnamServerError::Daemon)?; Ok(Self { tempdir, chunks, daemon: Some(daemon), url: format!("https://localhost:{}", port), }) } pub fn url(&self) -> String { self.url.clone() } pub fn stop(&mut self) { self.daemon.take(); } pub fn chunks(&self) -> &Path { &self.chunks } } fn write(filename: &Path, data: &[u8]) -> Result<(), ObnamServerError> { std::fs::write(filename, data) .map_err(|err| ObnamServerError::WriteTls(filename.to_path_buf(), err)) } fn pick_port() -> Result { let started = Instant::now(); while started.elapsed().as_millis() < TIMEOUT_MS { let port: PortNumber = random(); if PORT_RANGE.contains(&port) { return Ok(port); } } Err(ObnamServerError::Port) } #[derive(Debug, Serialize)] pub struct ServerConfig { address: String, chunks: PathBuf, tls_key: PathBuf, tls_cert: PathBuf, } impl ServerConfig { fn new(port: u16, chunks: PathBuf, tls_key: PathBuf, tls_cert: PathBuf) -> Self { Self { address: format!("localhost:{}", port), chunks, tls_key, tls_cert, } } fn write(&self, filename: &Path) -> Result<(), ObnamServerError> { let config = serde_yaml::to_string(self).unwrap(); debug!("server config:\n{}\n(end config)", config); std::fs::write(filename, &config) .map_err(|err| ObnamServerError::WriteConfig(filename.to_path_buf(), err))?; Ok(()) } }