diff options
Diffstat (limited to '')
| -rw-r--r-- | crates/common/Cargo.toml | 69 | ||||
| -rw-r--r-- | crates/common/src/lib.rs | 1 | ||||
| -rw-r--r-- | crates/common/src/shell.rs | 4 | ||||
| -rw-r--r-- | crates/common/src/tls.rs | 15 | ||||
| -rw-r--r-- | crates/daemon/src/aclient/api_client.rs | 17 | ||||
| -rw-r--r-- | crates/server/Cargo.toml | 68 | ||||
| -rw-r--r-- | crates/server/src/database/db/mod.rs | 10 | ||||
| -rw-r--r-- | crates/server/src/database/db/wrappers.rs | 2 | ||||
| -rw-r--r-- | crates/server/src/database/mod.rs | 18 | ||||
| -rw-r--r-- | crates/server/src/database/models.rs | 4 | ||||
| -rw-r--r-- | crates/server/src/handlers/mod.rs | 4 | ||||
| -rw-r--r-- | crates/server/src/lib.rs | 86 | ||||
| -rw-r--r-- | crates/server/src/main.rs | 88 | ||||
| -rw-r--r-- | crates/server/src/router.rs | 6 | ||||
| -rw-r--r-- | crates/server/src/settings.rs | 30 |
15 files changed, 142 insertions, 280 deletions
diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 3d36429c..df978c03 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -12,87 +12,18 @@ homepage = { workspace = true } repository = { workspace = true } [dependencies] -async-trait = "0.1.58" -axum = "0.8" -base64 = "0.22" -clap = { version = "4.5.7", features = ["derive"] } -clap_complete = "4.5.8" -clap_complete_nushell = "4.5.4" -colored = "2.0.4" -config = { version = "0.15.8", default-features = false, features = ["toml"] } -crossterm = {version = "0.29.0", features = ["use-dev-tty", "serde"] } -crypto_secretbox = "0.1.1" -dashmap = "6.1.0" directories = "6.0.0" eyre = "0.6" -fs-err = "3.1" -fs4 = "0.13.1" -fuzzy-matcher = "0.3.7" -hyper-util = "0.1" -indicatif = "0.18.0" -interim = { version = "0.2.0", features = ["time_0_3"] } -itertools = "0.14.0" -lasso = { version = "0.7", features = ["multi-threaded"] } -log = "0.4" -metrics = "0.24" -metrics-exporter-prometheus = { version = "0.18", default-features = false } -minspan = "0.1.5" -norm = { version = "0.1.1", features = ["fzf-v2"] } -notify = "7" -prost = "0.14" -rand = { version = "0.8.5", features = ["std"] } -ratatui = "0.30.0" -regex = "1.10.5" -reqwest = { version = "0.13", features = ["json", "rustls-no-provider", "stream"], default-features = false } -rmp = { version = "0.8.14" } -runtime-format = "0.1.3" -rustix = { version = "1.1.4", features = ["process", "fs"] } -rustls = { version = "0.23", default-features = false, features = [ "ring", "std", "tls12", ] } -rusty_paserk = { version = "0.5.0", default-features = false, features = [ "v4", "serde", ] } -rusty_paseto = { version = "0.8.0", default-features = false } semver = "1.0.20" serde = { version = "1.0.202", features = ["derive"] } -serde_json = "1.0.119" -serde_regex = "1.1.0" -serde_with = "3.8.1" -shellexpand = "3" -sql-builder = "3" sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "time", "postgres", "uuid", "sqlite", "regexp"] } -thiserror = "2" time = { version = "0.3.47", features = [ "serde-human-readable", "macros", "local-offset", "macros", "formatting", "parsing"] } -tokio = { version = "1", features = ["full"] } -tokio-stream = { version = "0.1.14", features = ["net"] } -toml_edit = "0.25.4" -tonic = "0.14" -tonic-prost = "0.14" -tower = "0.5" -tower-http = { version = "0.6", features = ["trace"] } -tracing = "0.1" -tracing-appender = "0.2" -tracing-subscriber = { version = "0.3", features = ["ansi", "fmt", "registry", "env-filter", "json"] } typed-builder = "0.18.2" -unicode-segmentation = "1.11.0" -unicode-width = "0.2" -url = "2.5.2" uuid = { version = "1.9", features = ["v4", "v7", "serde"] } -vt100 = "0.16" -whoami = "2.1.0" - -[target.'cfg(target_os = "linux")'.dependencies] -arboard = { version = "3.4", default-features = false, features = [ "wayland-data-control", ] } -listenfd = "1.0.1" - -[target.'cfg(unix)'.dependencies] -daemonize = "0.5.0" -portable-pty = "0.9" -signal-hook = "0.3" [dev-dependencies] -tokio = { version = "1", features = ["full"] } [build-dependencies] -protox = "0.9" -tonic-prost-build = "0.14" [package.metadata.docs.rs] all-features = true diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index d886520d..8bd18456 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -54,5 +54,4 @@ macro_rules! new_uuid { pub mod api; pub mod record; pub mod shell; -pub mod tls; pub mod utils; diff --git a/crates/common/src/shell.rs b/crates/common/src/shell.rs index 8bab806a..a57250e2 100644 --- a/crates/common/src/shell.rs +++ b/crates/common/src/shell.rs @@ -1,4 +1,4 @@ -#[derive(PartialEq)] +#[derive(PartialEq, Clone, Copy, Debug)] pub enum Shell { Sh, Bash, @@ -30,12 +30,14 @@ impl std::fmt::Display for Shell { } impl Shell { + #[must_use] pub fn from_env() -> Self { std::env::var("ATUIN_SHELL").map_or(Self::Unknown, |shell| { Self::from_string(shell.trim().to_lowercase().as_str()) }) } + #[must_use] pub fn from_string(name: &str) -> Self { match name { "bash" => Self::Bash, diff --git a/crates/common/src/tls.rs b/crates/common/src/tls.rs deleted file mode 100644 index e8c840e0..00000000 --- a/crates/common/src/tls.rs +++ /dev/null @@ -1,15 +0,0 @@ -use std::sync::Once; - -static INIT: Once = Once::new(); - -/// Ensure the rustls crypto provider (ring) is installed. -/// -/// Must be called before creating any reqwest clients. Safe to call -/// multiple times — only the first call installs the provider. -pub fn ensure_crypto_provider() { - INIT.call_once(|| { - rustls::crypto::ring::default_provider() - .install_default() - .expect("Failed to install rustls crypto provider"); - }); -} diff --git a/crates/daemon/src/aclient/api_client.rs b/crates/daemon/src/aclient/api_client.rs index 666e6dce..c0688cf9 100644 --- a/crates/daemon/src/aclient/api_client.rs +++ b/crates/daemon/src/aclient/api_client.rs @@ -10,7 +10,6 @@ use turtle_common::{api::ErrorResponse, record::RecordStatus}; use turtle_common::{ api::{ATUIN_CARGO_VERSION, ATUIN_HEADER_VERSION, ATUIN_VERSION}, record::{EncryptedData, HostId, Record, RecordIdx}, - tls::ensure_crypto_provider, }; use semver::Version; @@ -101,6 +100,22 @@ async fn handle_resp_error(resp: Response) -> Result<Response> { Ok(resp) } +use std::sync::Once; + +static INIT: Once = Once::new(); + +/// Ensure the rustls crypto provider (ring) is installed. +/// +/// Must be called before creating any reqwest clients. Safe to call +/// multiple times — only the first call installs the provider. +fn ensure_crypto_provider() { + INIT.call_once(|| { + rustls::crypto::ring::default_provider() + .install_default() + .expect("Failed to install rustls crypto provider"); + }); +} + impl<'a> Client<'a> { pub(crate) fn new( sync_addr: &'a str, diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 3a01d4fd..0afd678b 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -12,88 +12,24 @@ homepage = { workspace = true } repository = { workspace = true } [dependencies] -turtle-common = { workspace = true } -async-trait = "0.1.58" axum = "0.8" -base64 = "0.22" -clap = { version = "4.5.7", features = ["derive"] } -clap_complete = "4.5.8" -clap_complete_nushell = "4.5.4" -colored = "2.0.4" config = { version = "0.15.8", default-features = false, features = ["toml"] } -crossterm = {version = "0.29.0", features = ["use-dev-tty", "serde"] } -crypto_secretbox = "0.1.1" -dashmap = "6.1.0" -directories = "6.0.0" +clap = { workspace = true } eyre = "0.6" fs-err = "3.1" -fs4 = "0.13.1" -fuzzy-matcher = "0.3.7" -hyper-util = "0.1" -indicatif = "0.18.0" -interim = { version = "0.2.0", features = ["time_0_3"] } -itertools = "0.14.0" -lasso = { version = "0.7", features = ["multi-threaded"] } -log = "0.4" metrics = "0.24" metrics-exporter-prometheus = { version = "0.18", default-features = false } -minspan = "0.1.5" -norm = { version = "0.1.1", features = ["fzf-v2"] } -notify = "7" -prost = "0.14" rand = { version = "0.8.5", features = ["std"] } -ratatui = "0.30.0" -regex = "1.10.5" -reqwest = { version = "0.13", features = ["json", "rustls-no-provider", "stream"], default-features = false } -rmp = { version = "0.8.14" } -runtime-format = "0.1.3" -rustix = { version = "1.1.4", features = ["process", "fs"] } -rustls = { version = "0.23", default-features = false, features = [ "ring", "std", "tls12", ] } -rusty_paserk = { version = "0.5.0", default-features = false, features = [ "v4", "serde", ] } -rusty_paseto = { version = "0.8.0", default-features = false } -semver = "1.0.20" serde = { version = "1.0.202", features = ["derive"] } -serde_json = "1.0.119" -serde_regex = "1.1.0" -serde_with = "3.8.1" -shellexpand = "3" -sql-builder = "3" sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "time", "postgres", "uuid", "sqlite", "regexp"] } -thiserror = "2" time = { version = "0.3.47", features = [ "serde-human-readable", "macros", "local-offset", "macros", "formatting", "parsing"] } tokio = { version = "1", features = ["full"] } -tokio-stream = { version = "0.1.14", features = ["net"] } -toml_edit = "0.25.4" -tonic = "0.14" -tonic-prost = "0.14" tower = "0.5" tower-http = { version = "0.6", features = ["trace"] } tracing = "0.1" -tracing-appender = "0.2" -tracing-subscriber = { version = "0.3", features = ["ansi", "fmt", "registry", "env-filter", "json"] } -typed-builder = "0.18.2" -unicode-segmentation = "1.11.0" -unicode-width = "0.2" +turtle-common = { workspace = true } url = "2.5.2" uuid = { version = "1.9", features = ["v4", "v7", "serde"] } -vt100 = "0.16" -whoami = "2.1.0" - -[target.'cfg(target_os = "linux")'.dependencies] -arboard = { version = "3.4", default-features = false, features = [ "wayland-data-control", ] } -listenfd = "1.0.1" - -[target.'cfg(unix)'.dependencies] -daemonize = "0.5.0" -portable-pty = "0.9" -signal-hook = "0.3" - -[dev-dependencies] -tokio = { version = "1", features = ["full"] } - -[build-dependencies] -protox = "0.9" -tonic-prost-build = "0.14" [package.metadata.docs.rs] all-features = true diff --git a/crates/server/src/database/db/mod.rs b/crates/server/src/database/db/mod.rs index c95a2ed4..9345ff6b 100644 --- a/crates/server/src/database/db/mod.rs +++ b/crates/server/src/database/db/mod.rs @@ -15,7 +15,7 @@ mod wrappers; const MIN_PG_VERSION: u32 = 14; #[derive(Clone)] -pub struct ServerPostgres { +pub(crate) struct ServerPostgres { pool: sqlx::Pool<sqlx::postgres::Postgres>, /// Optional read replica pool for read-only queries read_pool: Option<sqlx::Pool<sqlx::postgres::Postgres>>, @@ -30,7 +30,7 @@ impl ServerPostgres { } impl ServerPostgres { - pub async fn new(settings: &DbSettings) -> DbResult<Self> { + pub(crate) async fn new(settings: &DbSettings) -> DbResult<Self> { let pool = PgPoolOptions::new() .max_connections(100) .connect(settings.db_uri.as_str()) @@ -91,7 +91,7 @@ impl ServerPostgres { } #[instrument(skip_all)] - pub async fn add_records( + pub(crate) async fn add_records( &self, user: &User, records: &[Record<EncryptedData>], @@ -167,7 +167,7 @@ impl ServerPostgres { } #[instrument(skip_all)] - pub async fn next_records( + pub(crate) async fn next_records( &self, user: &User, host: HostId, @@ -220,7 +220,7 @@ impl ServerPostgres { Ok(ret) } - pub async fn status(&self, user: &User) -> DbResult<RecordStatus> { + pub(crate) async fn status(&self, user: &User) -> DbResult<RecordStatus> { // If IDX_CACHE_ROLLOUT is set, then we // 1. Read the value of the var, use it as a % chance of using the cache // 2. If we use the cache, just read from the cache table diff --git a/crates/server/src/database/db/wrappers.rs b/crates/server/src/database/db/wrappers.rs index 8054289a..6bacf47f 100644 --- a/crates/server/src/database/db/wrappers.rs +++ b/crates/server/src/database/db/wrappers.rs @@ -1,7 +1,7 @@ use turtle_common::record::{EncryptedData, Host, Record}; use sqlx::{Row, postgres::PgRow}; -pub struct DbRecord(pub Record<EncryptedData>); +pub(crate) struct DbRecord(pub(crate) Record<EncryptedData>); impl<'a> ::sqlx::FromRow<'a, PgRow> for DbRecord { fn from_row(row: &'a PgRow) -> ::sqlx::Result<Self> { diff --git a/crates/server/src/database/mod.rs b/crates/server/src/database/mod.rs index c05fa783..43fe5c3b 100644 --- a/crates/server/src/database/mod.rs +++ b/crates/server/src/database/mod.rs @@ -1,12 +1,12 @@ -pub mod db; -pub mod models; +pub(crate) mod db; +pub(crate) mod models; use std::fmt::{Debug, Display}; use serde::{Deserialize, Serialize}; #[derive(Debug)] -pub enum DbError { +pub(crate) enum DbError { NotFound, Other(eyre::Report), } @@ -43,24 +43,24 @@ impl From<sqlx::Error> for DbError { impl std::error::Error for DbError {} -pub type DbResult<T> = Result<T, DbError>; +pub(crate) type DbResult<T> = Result<T, DbError>; #[derive(Debug, PartialEq)] -pub enum DbType { +pub(crate) enum DbType { Postgres, Unknown, } #[derive(Clone, Deserialize, Serialize)] -pub struct DbSettings { - pub db_uri: String, +pub(crate) struct DbSettings { + pub(crate) db_uri: String, /// Optional URI for read replicas. If set, read-only queries will use this connection. - pub read_db_uri: Option<String>, + pub(crate) read_db_uri: Option<String>, } impl DbSettings { - pub fn db_type(&self) -> DbType { + pub(crate) fn db_type(&self) -> DbType { if self.db_uri.starts_with("postgres://") || self.db_uri.starts_with("postgresql://") { DbType::Postgres } else { diff --git a/crates/server/src/database/models.rs b/crates/server/src/database/models.rs index 9f6241ae..3fa6f471 100644 --- a/crates/server/src/database/models.rs +++ b/crates/server/src/database/models.rs @@ -1,5 +1,5 @@ use uuid::Uuid; -pub struct User { - pub id: Uuid, +pub(crate) struct User { + pub(crate) id: Uuid, } diff --git a/crates/server/src/handlers/mod.rs b/crates/server/src/handlers/mod.rs index 1ac5a0c4..5fb5d0f2 100644 --- a/crates/server/src/handlers/mod.rs +++ b/crates/server/src/handlers/mod.rs @@ -29,8 +29,8 @@ impl IntoResponse for ErrorResponseStatus<'_> { } pub(crate) struct ErrorResponseStatus<'a> { - pub error: ErrorResponse<'a>, - pub status: http::StatusCode, + pub(crate) error: ErrorResponse<'a>, + pub(crate) status: http::StatusCode, } pub(crate) trait RespExt<'a> { diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs deleted file mode 100644 index 726afeea..00000000 --- a/crates/server/src/lib.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::future::Future; -use std::net::SocketAddr; - -use axum::{Router, serve}; -use database::db::ServerPostgres; -use eyre::{Context, Result}; - -pub mod database; -mod handlers; -mod metrics; -mod router; - -pub use settings::Settings; - -pub mod settings; - -use tokio::net::TcpListener; -use tokio::signal; - -#[cfg(target_family = "unix")] -async fn shutdown_signal() { - let mut term = signal::unix::signal(signal::unix::SignalKind::terminate()) - .expect("failed to register signal handler"); - let mut interrupt = signal::unix::signal(signal::unix::SignalKind::interrupt()) - .expect("failed to register signal handler"); - - tokio::select! { - _ = term.recv() => {}, - _ = interrupt.recv() => {}, - }; - eprintln!("Shutting down gracefully..."); -} - -pub async fn launch(settings: Settings, addr: SocketAddr) -> Result<()> { - launch_with_tcp_listener( - settings, - TcpListener::bind(addr) - .await - .context("could not connect to socket")?, - shutdown_signal(), - ) - .await -} - -pub async fn launch_with_tcp_listener( - settings: Settings, - listener: TcpListener, - shutdown: impl Future<Output = ()> + Send + 'static, -) -> Result<()> { - let r = make_router(settings).await?; - - serve(listener, r.into_make_service()) - .with_graceful_shutdown(shutdown) - .await?; - - Ok(()) -} - -// The separate listener means it's much easier to ensure metrics are not accidentally exposed to -// the public. -pub async fn launch_metrics_server(host: String, port: u16) -> Result<()> { - let listener = TcpListener::bind((host, port)) - .await - .context("failed to bind metrics tcp")?; - - let recorder_handle = metrics::setup_metrics_recorder(); - - let router = Router::new().route( - "/metrics", - axum::routing::get(move || std::future::ready(recorder_handle.render())), - ); - - serve(listener, router.into_make_service()) - .with_graceful_shutdown(shutdown_signal()) - .await?; - - Ok(()) -} - -async fn make_router(settings: Settings) -> Result<Router, eyre::Error> { - let db = ServerPostgres::new(&settings.db_settings) - .await - .wrap_err_with(|| format!("failed to connect to db: {:?}", settings.db_settings))?; - let r = router::router(db, settings); - Ok(r) -} diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index 94d6d143..fb587754 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -1,13 +1,25 @@ +use std::future::Future; use std::net::SocketAddr; -use turtle_server::{Settings, database::DbType, launch, launch_metrics_server}; - +use axum::{Router, serve}; use clap::Parser; +use database::db::ServerPostgres; use eyre::{Context, Result, eyre}; +use tokio::net::TcpListener; +use tokio::signal; + +use crate::database::DbType; +use crate::settings::Settings; + +mod database; +mod handlers; +mod metrics; +mod router; +mod settings; #[derive(Parser, Clone, Debug)] #[command(infer_subcommands = true)] -pub enum Cmd { +pub(crate) enum Cmd { /// Start the server Start { /// The host address to bind @@ -24,7 +36,7 @@ pub enum Cmd { } impl Cmd { - pub async fn run(self) -> Result<()> { + async fn run(self) -> Result<()> { match self { Self::Start { host, port } => { let settings = Settings::new().wrap_err("could not load server settings")?; @@ -59,3 +71,71 @@ impl Cmd { async fn main() -> Result<()> { Cmd::parse().run().await } + +#[cfg(target_family = "unix")] +async fn shutdown_signal() { + let mut term = signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to register signal handler"); + let mut interrupt = signal::unix::signal(signal::unix::SignalKind::interrupt()) + .expect("failed to register signal handler"); + + tokio::select! { + _ = term.recv() => {}, + _ = interrupt.recv() => {}, + }; + eprintln!("Shutting down gracefully..."); +} + +async fn launch(settings: Settings, addr: SocketAddr) -> Result<()> { + launch_with_tcp_listener( + settings, + TcpListener::bind(addr) + .await + .context("could not connect to socket")?, + shutdown_signal(), + ) + .await +} + +async fn launch_with_tcp_listener( + settings: Settings, + listener: TcpListener, + shutdown: impl Future<Output = ()> + Send + 'static, +) -> Result<()> { + let r = make_router(settings).await?; + + serve(listener, r.into_make_service()) + .with_graceful_shutdown(shutdown) + .await?; + + Ok(()) +} + +// The separate listener means it's much easier to ensure metrics are not accidentally exposed to +// the public. +async fn launch_metrics_server(host: String, port: u16) -> Result<()> { + let listener = TcpListener::bind((host, port)) + .await + .context("failed to bind metrics tcp")?; + + let recorder_handle = metrics::setup_metrics_recorder(); + + let router = Router::new().route( + "/metrics", + axum::routing::get(move || std::future::ready(recorder_handle.render())), + ); + + serve(listener, router.into_make_service()) + .with_graceful_shutdown(shutdown_signal()) + .await?; + + Ok(()) +} + +async fn make_router(settings: Settings) -> Result<Router, eyre::Error> { + let db = ServerPostgres::new(&settings.db_settings) + .await + .wrap_err_with(|| format!("failed to connect to db: {:?}", settings.db_settings))?; + let r = router::router(db, settings); + Ok(r) +} diff --git a/crates/server/src/router.rs b/crates/server/src/router.rs index ce12f5b6..5ba15e62 100644 --- a/crates/server/src/router.rs +++ b/crates/server/src/router.rs @@ -21,7 +21,7 @@ use crate::{ settings::Settings, }; -pub(crate) struct UserAuth(pub User); +pub(crate) struct UserAuth(pub(crate) User); impl FromRequestParts<AppState> for UserAuth { type Rejection = ErrorResponseStatus<'static>; @@ -66,8 +66,8 @@ async fn semver(request: Request, next: Next) -> Response { #[derive(Clone)] pub(crate) struct AppState { - pub database: ServerPostgres, - pub settings: Settings, + pub(crate) database: ServerPostgres, + pub(crate) settings: Settings, } pub(crate) fn router(database: ServerPostgres, settings: Settings) -> Router { diff --git a/crates/server/src/settings.rs b/crates/server/src/settings.rs index 256aa661..60837525 100644 --- a/crates/server/src/settings.rs +++ b/crates/server/src/settings.rs @@ -9,11 +9,11 @@ use tracing::info; use crate::database::DbSettings; #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Metrics { +pub(crate) struct Metrics { #[serde(alias = "enabled")] - pub enable: bool, - pub host: String, - pub port: u16, + pub(crate) enable: bool, + pub(crate) host: String, + pub(crate) port: u16, } impl Default for Metrics { @@ -27,29 +27,29 @@ impl Default for Metrics { } #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Settings { - pub host: String, - pub port: u16, - pub path: String, - pub max_history_length: usize, - pub max_record_size: usize, - pub page_size: i64, - pub metrics: Metrics, +pub(crate) struct Settings { + pub(crate) host: String, + pub(crate) port: u16, + pub(crate) path: String, + pub(crate) max_history_length: usize, + pub(crate) max_record_size: usize, + pub(crate) page_size: i64, + pub(crate) metrics: Metrics, /// Advertise a version that is not what we are _actually_ running /// Many clients compare their version with api.atuin.sh, and if they differ, notify the user /// that an update is available. /// Now that we take beta releases, we should be able to advertise a different version to avoid /// notifying users when the server runs something that is not a stable release. - pub fake_version: Option<String>, + pub(crate) fake_version: Option<String>, #[serde(flatten)] #[expect(clippy::struct_field_names)] - pub db_settings: DbSettings, + pub(crate) db_settings: DbSettings, } impl Settings { - pub fn new() -> Result<Self> { + pub(crate) fn new() -> Result<Self> { // create the config file if it does not exist let mut config_builder = Config::builder() .set_default("host", "127.0.0.1")? |
