1use std::time::Duration;
2
3use miette::{IntoDiagnostic, Report, Result, WrapErr, miette};
4use mobc::{Connection, Pool};
5use tokio_postgres::config::SslMode;
6use tracing::debug;
7
8pub use manager::{PgConnectionManager, PgError};
9
10mod manager;
11mod tls;
12mod url;
13
14fn is_tls_error(error: &Report) -> bool {
16 if error.downcast_ref::<rustls::Error>().is_some() {
17 return true;
18 }
19
20 let mut source = error.source();
22 while let Some(err) = source {
23 if err.downcast_ref::<rustls::Error>().is_some() {
24 return true;
25 }
26 source = err.source();
27 }
28
29 let message = error.to_string();
30 message.contains("tls:")
31 || message.contains("rustls")
32 || message.contains("certificate")
33 || message.contains("TLS handshake")
34 || message.contains("invalid configuration")
35}
36
37fn is_auth_error(error: &Report) -> bool {
39 if let Some(db_error) = error.downcast_ref::<tokio_postgres::Error>()
40 && let Some(db_error) = db_error.as_db_error()
41 {
42 let code = db_error.code().code();
46 return code == "28000" || code == "28P01";
47 }
48
49 let message = error.to_string();
51 message.contains("password authentication failed")
52 || message.contains("no password supplied")
53 || message.contains("authentication failed")
54}
55
56pub type PgConnection = Connection<manager::PgConnectionManager>;
57
58#[derive(Debug, Clone)]
59pub struct PgPool {
60 pub manager: manager::PgConnectionManager,
61 pub inner: Pool<manager::PgConnectionManager>,
62}
63
64impl PgPool {
65 pub async fn get(&self) -> Result<PgConnection, mobc::Error<PgError>> {
69 self.inner.get().await
70 }
71
72 pub async fn get_timeout(
77 &self,
78 duration: Duration,
79 ) -> Result<PgConnection, mobc::Error<PgError>> {
80 self.inner.get_timeout(duration).await
81 }
82}
83
84pub async fn create_pool(url: &str, application_name: &str) -> Result<PgPool> {
99 create_pool_sized(url, application_name, PoolSize::default(), Prompt::Allowed).await
100}
101
102#[derive(Clone, Copy, Debug)]
112pub struct PoolSize {
113 pub max_open: u64,
114 pub max_idle: u64,
115 pub max_idle_lifetime: Option<Duration>,
120 pub get_timeout: Option<Duration>,
127 pub connect_timeout: Option<Duration>,
134}
135
136impl Default for PoolSize {
137 fn default() -> Self {
138 Self {
140 max_open: 10,
141 max_idle: 10,
142 max_idle_lifetime: None,
143 get_timeout: Some(Duration::from_secs(30)),
144 connect_timeout: None,
145 }
146 }
147}
148
149#[derive(Clone, Copy, Debug, PartialEq, Eq)]
156pub enum Prompt {
157 Allowed,
158 Never,
159}
160
161pub async fn create_pool_sized(
163 url: &str,
164 application_name: &str,
165 size: PoolSize,
166 prompt: Prompt,
167) -> Result<PgPool> {
168 let mut config = url::parse_connection_url(url)?;
169
170 config.application_name(application_name);
171 if let Some(timeout) = size.connect_timeout {
172 config.connect_timeout(timeout);
173 }
174
175 let mut tried_ssl_fallback = false;
176
177 let pool = loop {
179 debug!("Creating manager");
180 let tls = config.get_ssl_mode() != SslMode::Disable;
181 let manager = crate::pool::PgConnectionManager::new(config.clone(), tls);
182
183 debug!("Creating pool");
184 let pool = Pool::builder()
185 .max_lifetime(Some(Duration::from_secs(3600)))
186 .max_open(size.max_open)
187 .max_idle(size.max_idle)
188 .max_idle_lifetime(size.max_idle_lifetime)
189 .get_timeout(size.get_timeout)
190 .build(manager.clone());
191
192 let pool = PgPool {
193 manager,
194 inner: pool,
195 };
196
197 debug!("Checking pool");
198 match check_pool(&pool).await {
199 Ok(_) => {
200 if tried_ssl_fallback {
201 tracing::info!("Connected successfully with SSL disabled after TLS error");
202 }
203 break pool;
204 }
205 Err(e) => {
206 debug!("Connection error: {:#}", e);
207 debug!(
208 "is_tls_error: {}, is_auth_error: {}",
209 is_tls_error(&e),
210 is_auth_error(&e)
211 );
212
213 if is_tls_error(&e) {
214 if config.get_ssl_mode() == SslMode::Prefer && !tried_ssl_fallback {
216 debug!("TLS failed with prefer mode, retrying with SSL disabled");
217 config.ssl_mode(SslMode::Disable);
218 tried_ssl_fallback = true;
219 continue;
220 }
221
222 return Err(e).wrap_err(
224 "TLS/SSL connection failed. Try using --ssl disable, \
225 or use a connection URL with sslmode=disable: \
226 postgresql://user@host/db?sslmode=disable",
227 );
228 } else if is_auth_error(&e)
229 && config.get_password().is_none()
230 && prompt == Prompt::Allowed
231 {
232 let password = rpassword::prompt_password("Password: ").into_diagnostic()?;
233 config.password(password);
234 } else {
236 return Err(e);
238 }
239 }
240 }
241 };
242
243 Ok(pool)
244}
245
246pub async fn connect_one(url: &str, application_name: &str) -> Result<tokio_postgres::Client> {
255 connect_one_with(url, application_name, Prompt::Allowed).await
256}
257
258pub async fn connect_one_with(
260 url: &str,
261 application_name: &str,
262 prompt: Prompt,
263) -> Result<tokio_postgres::Client> {
264 use mobc::Manager as _;
265
266 let pool = create_pool_sized(url, application_name, PoolSize::default(), prompt).await?;
267 pool.manager.connect().await.into_diagnostic()
268}
269
270async fn check_pool(pool: &PgPool) -> Result<()> {
272 let conn = match pool.get().await {
273 Err(mobc::Error::Inner(db_err)) => {
274 return Err(match db_err.as_db_error() {
275 Some(db_err) => miette!(
276 "E{code} at {func} in {file}:{line}",
277 code = db_err.code().code(),
278 func = db_err.routine().unwrap_or("{unknown}"),
279 file = db_err.file().unwrap_or("unknown.c"),
280 line = db_err.line().unwrap_or(0)
281 ),
282 _ => miette!("{db_err}"),
283 })
284 .wrap_err(
285 db_err
286 .as_db_error()
287 .map(|e| e.to_string())
288 .unwrap_or_default(),
289 )?;
290 }
291 res @ Err(_) => {
292 let res = res.map(drop).into_diagnostic();
293 return if let Err(ref err) = res
294 && is_auth_error(err)
295 {
296 res.wrap_err("hint: check the password")
297 } else {
298 res
299 };
300 }
301 Ok(conn) => conn,
302 };
303 conn.simple_query("SELECT 1")
304 .await
305 .into_diagnostic()
306 .wrap_err("checking connection")?;
307 Ok(())
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[tokio::test]
315 async fn test_create_pool_valid_connection_string() {
316 let connection_string = "postgresql://localhost/test";
317 let result = create_pool(connection_string, "test").await;
318 if let Err(e) = result {
320 let error_msg = format!("{:?}", e);
321 assert!(
322 !error_msg.contains("parsing connection string"),
323 "Should not be a parsing error: {}",
324 error_msg
325 );
326 }
327 }
328
329 #[tokio::test]
330 async fn test_create_pool_with_full_url() {
331 let connection_string = "postgresql://user:pass@localhost:5432/testdb";
332 let result = create_pool(connection_string, "test").await;
333 if let Err(e) = result {
335 let error_msg = format!("{:?}", e);
336 assert!(
337 !error_msg.contains("parsing connection string"),
338 "Should not be a parsing error: {}",
339 error_msg
340 );
341 }
342 }
343
344 #[tokio::test]
345 async fn test_create_pool_with_unix_socket_path() {
346 let url = "postgresql:///postgres?host=/var/run/postgresql";
348 let result = create_pool(url, "test").await;
349 match result {
352 Ok(_) => {
353 }
355 Err(e) => {
356 let error_msg = format!("{:?}", e);
357 assert!(
359 !error_msg.contains("parsing connection string"),
360 "Should not be a parsing error: {}",
361 error_msg
362 );
363 }
364 }
365 }
366
367 #[tokio::test]
368 async fn test_create_pool_with_encoded_unix_socket() {
369 let url = "postgresql://%2Fvar%2Frun%2Fpostgresql/postgres";
371 let result = create_pool(url, "test").await;
372 match result {
374 Ok(_) => {
375 }
377 Err(e) => {
378 let error_msg = format!("{:?}", e);
379 assert!(
381 !error_msg.contains("parsing connection string"),
382 "Should not be a parsing error: {}",
383 error_msg
384 );
385 }
386 }
387 }
388
389 #[tokio::test]
390 async fn test_create_pool_with_no_host() {
391 let url = "postgresql:///postgres";
393 let result = create_pool(url, "test").await;
394 match result {
396 Ok(_) => {
397 }
399 Err(e) => {
400 let error_msg = format!("{:?}", e);
401 assert!(
403 !error_msg.contains("parsing connection string"),
404 "Should not be a parsing error: {}",
405 error_msg
406 );
407 }
408 }
409 }
410
411 #[tokio::test]
412 async fn test_unix_socket_connection_end_to_end() {
413 let url = "postgresql:///postgres?host=/var/run/postgresql";
415 let result = create_pool(url, "test").await;
416
417 match result {
418 Ok(pool) => {
419 let conn = pool.get().await;
421 if let Ok(conn) = conn {
422 let result = conn.simple_query("SELECT 1 as test").await;
423 assert!(result.is_ok(), "Query should succeed");
424 }
425 }
426 Err(e) => {
427 let error_msg = format!("{:?}", e);
428 assert!(
430 !error_msg.contains("parsing connection string"),
431 "Should not be a parsing error: {}",
432 error_msg
433 );
434 assert!(
435 !error_msg.contains("TLS handshake"),
436 "Should not be a TLS error for Unix socket: {}",
437 error_msg
438 );
439 }
440 }
441 }
442}