Skip to main content

oxisql_postgres/
builder.rs

1//! Builder types for configuring PostgreSQL connections.
2//!
3//! This module provides:
4//!
5//! - [`TlsMode`] — selects between plain-text and TLS-encrypted connections.
6//! - [`PgConnectionBuilder`] — fluent builder API for [`crate::PgConnection`].
7
8use std::sync::Arc;
9
10use rustls_pki_types::CertificateDer;
11
12use crate::connection::PgConnection;
13use crate::error::PgError;
14
15// ── TLS mode ──────────────────────────────────────────────────────────────────
16
17/// TLS mode to use when connecting to PostgreSQL.
18#[derive(Clone, Default)]
19#[allow(missing_debug_implementations)]
20pub enum TlsMode {
21    /// Plain-text connection — no encryption.
22    #[default]
23    Disabled,
24    /// TLS via a pre-built `rustls` `ClientConfig` sourced from OxiTLS.
25    ///
26    /// Build the config using, e.g.:
27    /// ```rust,no_run
28    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
29    /// let cfg = oxitls::client_config(oxitls::webpki_root_certs())?;
30    /// let tls = oxisql_postgres::TlsMode::Rustls(cfg);
31    /// # Ok(())
32    /// # }
33    /// ```
34    Rustls(Arc<rustls::ClientConfig>),
35}
36
37impl TlsMode {
38    /// Build a `TlsMode::Rustls` that skips server certificate verification.
39    ///
40    /// # Security Warning
41    ///
42    /// This mode accepts **any** server certificate, including self-signed and
43    /// expired ones.  It is vulnerable to man-in-the-middle attacks.
44    /// **Only use this in development or testing environments.**
45    ///
46    /// # Errors
47    ///
48    /// Returns [`PgError::Tls`] if the `rustls` `ClientConfig` cannot be built.
49    pub fn skip_verify() -> Result<Self, PgError> {
50        let provider = oxitls::pure_provider();
51        let verifier = Arc::new(NoCertVerifier);
52        let cfg = rustls::ClientConfig::builder_with_provider(provider)
53            .with_safe_default_protocol_versions()
54            .map_err(|e| PgError::Tls(e.to_string()))?
55            .dangerous()
56            .with_custom_certificate_verifier(verifier)
57            .with_no_client_auth();
58        Ok(TlsMode::Rustls(Arc::new(cfg)))
59    }
60
61    /// Build a `TlsMode::Rustls` that trusts a custom CA certificate in PEM
62    /// format, in addition to the system/WebPKI root store.
63    ///
64    /// `ca_pem` must contain one or more PEM-encoded X.509 certificates.
65    ///
66    /// # Errors
67    ///
68    /// Returns [`PgError::Tls`] if the PEM cannot be parsed or the
69    /// `rustls` `ClientConfig` cannot be built.
70    pub fn with_ca_pem(ca_pem: Vec<u8>) -> Result<Self, PgError> {
71        use std::io::BufReader;
72
73        let mut root_store = rustls::RootCertStore::empty();
74        // Trust the Mozilla WebPKI bundle first.
75        root_store.extend(oxitls::webpki_root_certs().roots);
76
77        // Parse and add the caller-supplied CA.
78        let mut reader = BufReader::new(ca_pem.as_slice());
79        let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut reader)
80            .collect::<Result<Vec<_>, _>>()
81            .map_err(|e| PgError::Tls(format!("PEM parse error: {e}")))?;
82        if certs.is_empty() {
83            return Err(PgError::Tls("no certificates found in ca_pem".to_string()));
84        }
85        for cert in certs {
86            root_store
87                .add(cert)
88                .map_err(|e| PgError::Tls(format!("bad CA cert: {e}")))?;
89        }
90
91        let provider = oxitls::pure_provider();
92        let cfg = rustls::ClientConfig::builder_with_provider(provider)
93            .with_safe_default_protocol_versions()
94            .map_err(|e| PgError::Tls(e.to_string()))?
95            .with_root_certificates(root_store)
96            .with_no_client_auth();
97        Ok(TlsMode::Rustls(Arc::new(cfg)))
98    }
99}
100
101// ── NoCertVerifier ────────────────────────────────────────────────────────────
102
103/// A certificate verifier that accepts all server certificates without any
104/// validation.
105///
106/// **For development and testing only.** Using this in production makes TLS
107/// connections trivially vulnerable to man-in-the-middle attacks.
108#[derive(Debug)]
109struct NoCertVerifier;
110
111impl rustls::client::danger::ServerCertVerifier for NoCertVerifier {
112    fn verify_server_cert(
113        &self,
114        _end_entity: &CertificateDer<'_>,
115        _intermediates: &[CertificateDer<'_>],
116        _server_name: &rustls_pki_types::ServerName<'_>,
117        _ocsp_response: &[u8],
118        _now: rustls_pki_types::UnixTime,
119    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
120        Ok(rustls::client::danger::ServerCertVerified::assertion())
121    }
122
123    fn verify_tls12_signature(
124        &self,
125        _message: &[u8],
126        _cert: &CertificateDer<'_>,
127        _dss: &rustls::DigitallySignedStruct,
128    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
129        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
130    }
131
132    fn verify_tls13_signature(
133        &self,
134        _message: &[u8],
135        _cert: &CertificateDer<'_>,
136        _dss: &rustls::DigitallySignedStruct,
137    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
138        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
139    }
140
141    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
142        oxitls::pure_provider()
143            .signature_verification_algorithms
144            .supported_schemes()
145    }
146}
147
148// ── PgConnectionBuilder ───────────────────────────────────────────────────────
149
150/// Builder for configuring and establishing a PostgreSQL connection.
151///
152/// # Example
153///
154/// ```rust,no_run
155/// # #[tokio::main]
156/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
157/// use oxisql_postgres::{PgConnectionBuilder, TlsMode};
158///
159/// let conn = PgConnectionBuilder::new()
160///     .host("localhost")
161///     .port(5432)
162///     .dbname("mydb")
163///     .user("postgres")
164///     .password("secret")
165///     .connect()
166///     .await?;
167/// # Ok(())
168/// # }
169/// ```
170#[derive(Default)]
171pub struct PgConnectionBuilder {
172    host: Option<String>,
173    port: Option<u16>,
174    dbname: Option<String>,
175    user: Option<String>,
176    password: Option<String>,
177    connect_timeout_secs: Option<u64>,
178    tls_mode: TlsMode,
179}
180
181impl PgConnectionBuilder {
182    /// Create a new builder with defaults.
183    pub fn new() -> Self {
184        Self::default()
185    }
186
187    /// Set the host.
188    pub fn host(mut self, h: impl Into<String>) -> Self {
189        self.host = Some(h.into());
190        self
191    }
192
193    /// Set the port.
194    pub fn port(mut self, p: u16) -> Self {
195        self.port = Some(p);
196        self
197    }
198
199    /// Set the database name.
200    pub fn dbname(mut self, db: impl Into<String>) -> Self {
201        self.dbname = Some(db.into());
202        self
203    }
204
205    /// Set the username.
206    pub fn user(mut self, u: impl Into<String>) -> Self {
207        self.user = Some(u.into());
208        self
209    }
210
211    /// Set the password.
212    pub fn password(mut self, pw: impl Into<String>) -> Self {
213        self.password = Some(pw.into());
214        self
215    }
216
217    /// Set the connection timeout in seconds.
218    pub fn connect_timeout_secs(mut self, secs: u64) -> Self {
219        self.connect_timeout_secs = Some(secs);
220        self
221    }
222
223    /// Set the TLS mode.
224    pub fn tls_mode(mut self, mode: TlsMode) -> Self {
225        self.tls_mode = mode;
226        self
227    }
228
229    /// Require TLS but skip server certificate verification.
230    ///
231    /// **INSECURE** — intended for development and testing environments only.
232    /// Production code should use [`tls_with_ca_pem`][Self::tls_with_ca_pem] or
233    /// supply a trusted root certificate store via
234    /// [`tls_mode`][Self::tls_mode] with a fully validated
235    /// [`TlsMode::Rustls`] config.
236    ///
237    /// Internally calls [`TlsMode::skip_verify`] to build a `ClientConfig`
238    /// that accepts all server certificates without validation.
239    ///
240    /// # Errors
241    ///
242    /// Returns [`PgError::Tls`] if the `rustls` `ClientConfig` cannot be built.
243    pub fn tls_skip_verify(mut self) -> Result<Self, PgError> {
244        self.tls_mode = TlsMode::skip_verify()?;
245        Ok(self)
246    }
247
248    /// Require TLS and verify the server certificate against a custom CA.
249    ///
250    /// `ca_pem` must be a PEM-encoded certificate authority certificate.
251    /// Multiple certificates may be concatenated in a single `Vec<u8>`.
252    ///
253    /// Internally calls [`TlsMode::with_ca_pem`] to build a `ClientConfig`
254    /// that trusts the supplied CA in addition to the Mozilla WebPKI bundle.
255    ///
256    /// # Errors
257    ///
258    /// Returns [`PgError::Tls`] if the PEM cannot be parsed or is empty.
259    pub fn tls_with_ca_pem(mut self, ca_pem: Vec<u8>) -> Result<Self, PgError> {
260        self.tls_mode = TlsMode::with_ca_pem(ca_pem)?;
261        Ok(self)
262    }
263
264    /// Establish the connection.
265    pub async fn connect(self) -> Result<PgConnection, PgError> {
266        let mut parts = Vec::new();
267        if let Some(h) = &self.host {
268            parts.push(format!("host={h}"));
269        }
270        if let Some(p) = self.port {
271            parts.push(format!("port={p}"));
272        }
273        if let Some(db) = &self.dbname {
274            parts.push(format!("dbname={db}"));
275        }
276        if let Some(u) = &self.user {
277            parts.push(format!("user={u}"));
278        }
279        if let Some(pw) = &self.password {
280            parts.push(format!("password={pw}"));
281        }
282        if let Some(t) = self.connect_timeout_secs {
283            parts.push(format!("connect_timeout={t}"));
284        }
285        let conn_str = parts.join(" ");
286        PgConnection::connect(&conn_str, self.tls_mode).await
287    }
288}