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}