use crate::password::Password;
#[derive(Debug, Clone, Default)]
pub enum TlsConfig {
#[default]
Verified,
Ca(Vec<u8>),
Insecure,
}
#[derive(Debug)]
pub struct DaemonServer {
pub host: String,
pub port: u16,
pub user: String,
pub password: Password,
pub tls: TlsConfig,
}
impl DaemonServer {
pub const DEFAULT_PORT: u16 = 8076;
#[must_use]
pub fn builder() -> DaemonServerBuilder {
DaemonServerBuilder::default()
}
}
#[cfg(feature = "insecure-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "insecure-tls")))]
impl DaemonServer {
pub async fn fetch_certificate(host: &str, port: u16) -> crate::Result<Vec<u8>> {
crate::transport::tls::fetch_certificate(host, port).await
}
}
#[derive(Debug, Default)]
pub struct DaemonServerBuilder {
host: Option<String>,
port: Option<u16>,
user: Option<String>,
password: Option<Password>,
tls: Option<TlsConfig>,
}
impl DaemonServerBuilder {
#[must_use]
pub fn host(mut self, host: impl Into<String>) -> Self {
self.host = Some(host.into());
self
}
#[must_use]
pub fn port(mut self, port: u16) -> Self {
self.port = Some(port);
self
}
#[must_use]
pub fn user(mut self, user: impl Into<String>) -> Self {
self.user = Some(user.into());
self
}
#[must_use]
pub fn password(mut self, password: String) -> Self {
self.password = Some(Password::new(password));
self
}
#[must_use]
pub fn tls(mut self, tls: TlsConfig) -> Self {
self.tls = Some(tls);
self
}
pub fn build(self) -> Result<DaemonServer, BuilderError> {
Ok(DaemonServer {
host: self.host.ok_or(BuilderError::MissingField("host"))?,
port: self.port.unwrap_or(DaemonServer::DEFAULT_PORT),
user: self.user.ok_or(BuilderError::MissingField("user"))?,
password: self
.password
.ok_or(BuilderError::MissingField("password"))?,
tls: self.tls.unwrap_or_default(),
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum BuilderError {
#[error("missing required field: {0}")]
MissingField(&'static str),
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
#[test]
fn default_is_verified() {
assert!(matches!(TlsConfig::default(), TlsConfig::Verified));
}
#[test]
fn ca_holds_bytes() {
let bytes = vec![0xAA, 0xBB, 0xCC];
let cfg = TlsConfig::Ca(bytes.clone());
match cfg {
TlsConfig::Ca(b) => assert_eq!(b, bytes),
_ => panic!("expected Ca variant"),
}
}
#[test]
fn builder_defaults_port_and_tls() {
let s = DaemonServer::builder()
.host("ibmi.example.com")
.user("DCURTIS")
.password("hunter2".to_string())
.build()
.expect("DaemonServer builds with all required fields set");
assert_eq!(s.host, "ibmi.example.com");
assert_eq!(s.port, DaemonServer::DEFAULT_PORT);
assert_eq!(s.user, "DCURTIS");
assert!(matches!(s.tls, TlsConfig::Verified));
}
#[test]
fn builder_missing_host_is_error() {
let err = DaemonServer::builder()
.user("DCURTIS")
.password("x".to_string())
.build()
.unwrap_err();
assert!(matches!(err, BuilderError::MissingField("host")));
}
#[test]
fn builder_missing_user_is_error() {
let err = DaemonServer::builder()
.host("h")
.password("x".to_string())
.build()
.unwrap_err();
assert!(matches!(err, BuilderError::MissingField("user")));
}
#[test]
fn builder_missing_password_is_error() {
let err = DaemonServer::builder()
.host("h")
.user("u")
.build()
.unwrap_err();
assert!(matches!(err, BuilderError::MissingField("password")));
}
#[test]
fn into_arc_works() {
let s = DaemonServer::builder()
.host("h")
.user("u")
.password("p".to_string())
.build()
.unwrap();
let a: Arc<DaemonServer> = s.into();
assert_eq!(a.host, "h");
}
#[test]
fn builder_overrides_port_and_tls() {
let s = DaemonServer::builder()
.host("h")
.user("u")
.password("p".to_string())
.port(9999)
.tls(TlsConfig::Insecure)
.build()
.expect("DaemonServer builds with all required fields set");
assert_eq!(s.port, 9999);
assert!(matches!(s.tls, TlsConfig::Insecure));
}
}
#[cfg(feature = "serde-config")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde-config")))]
#[derive(Debug, serde::Deserialize)]
pub struct DaemonServerSpec {
pub host: String,
#[serde(default)]
pub port: Option<u16>,
pub user: String,
pub password: String,
#[serde(default)]
pub tls: TlsConfigSpec,
}
#[cfg(feature = "serde-config")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde-config")))]
#[derive(Debug, Default, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TlsConfigSpec {
#[default]
Verified,
Ca(String),
Insecure,
}
#[cfg(feature = "serde-config")]
impl DaemonServerSpec {
pub fn try_into_server(self) -> Result<DaemonServer, SpecError> {
use base64::Engine;
let tls = match self.tls {
TlsConfigSpec::Verified => TlsConfig::Verified,
TlsConfigSpec::Insecure => TlsConfig::Insecure,
TlsConfigSpec::Ca(b64) => {
let bytes = base64::engine::general_purpose::STANDARD
.decode(&b64)
.map_err(SpecError::InvalidCaBase64)?;
TlsConfig::Ca(bytes)
}
};
Ok(DaemonServer {
host: self.host,
port: self.port.unwrap_or(DaemonServer::DEFAULT_PORT),
user: self.user,
password: Password::new(self.password),
tls,
})
}
}
#[cfg(feature = "serde-config")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde-config")))]
#[derive(Debug, thiserror::Error)]
pub enum SpecError {
#[error("invalid base64 in tls.ca: {0}")]
InvalidCaBase64(#[source] base64::DecodeError),
}
#[cfg(all(test, feature = "serde-config"))]
mod spec_tests {
use super::*;
#[test]
fn parses_minimal_json() {
let json = r#"{
"host": "ibmi.example.com",
"user": "DCURTIS",
"password": "hunter2"
}"#;
let spec: DaemonServerSpec =
serde_json::from_str(json).expect("DaemonServerSpec parses from JSON");
let server = spec
.try_into_server()
.expect("DaemonServerSpec converts to DaemonServer");
assert_eq!(server.host, "ibmi.example.com");
assert_eq!(server.port, DaemonServer::DEFAULT_PORT);
}
#[test]
fn parses_with_explicit_port_and_insecure_tls() {
let json = r#"{
"host": "h",
"port": 9000,
"user": "u",
"password": "p",
"tls": "insecure"
}"#;
let spec: DaemonServerSpec =
serde_json::from_str(json).expect("DaemonServerSpec parses from JSON");
let server = spec
.try_into_server()
.expect("DaemonServerSpec converts to DaemonServer");
assert_eq!(server.port, 9000);
assert!(matches!(server.tls, TlsConfig::Insecure));
}
}