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 use mobc::Manager as _;
256
257 let pool = create_pool(url, application_name).await?;
258 pool.manager.connect().await.into_diagnostic()
259}
260
261async fn check_pool(pool: &PgPool) -> Result<()> {
263 let conn = match pool.get().await {
264 Err(mobc::Error::Inner(db_err)) => {
265 return Err(match db_err.as_db_error() {
266 Some(db_err) => miette!(
267 "E{code} at {func} in {file}:{line}",
268 code = db_err.code().code(),
269 func = db_err.routine().unwrap_or("{unknown}"),
270 file = db_err.file().unwrap_or("unknown.c"),
271 line = db_err.line().unwrap_or(0)
272 ),
273 _ => miette!("{db_err}"),
274 })
275 .wrap_err(
276 db_err
277 .as_db_error()
278 .map(|e| e.to_string())
279 .unwrap_or_default(),
280 )?;
281 }
282 res @ Err(_) => {
283 let res = res.map(drop).into_diagnostic();
284 return if let Err(ref err) = res
285 && is_auth_error(err)
286 {
287 res.wrap_err("hint: check the password")
288 } else {
289 res
290 };
291 }
292 Ok(conn) => conn,
293 };
294 conn.simple_query("SELECT 1")
295 .await
296 .into_diagnostic()
297 .wrap_err("checking connection")?;
298 Ok(())
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[tokio::test]
306 async fn test_create_pool_valid_connection_string() {
307 let connection_string = "postgresql://localhost/test";
308 let result = create_pool(connection_string, "test").await;
309 if let Err(e) = result {
311 let error_msg = format!("{:?}", e);
312 assert!(
313 !error_msg.contains("parsing connection string"),
314 "Should not be a parsing error: {}",
315 error_msg
316 );
317 }
318 }
319
320 #[tokio::test]
321 async fn test_create_pool_with_full_url() {
322 let connection_string = "postgresql://user:pass@localhost:5432/testdb";
323 let result = create_pool(connection_string, "test").await;
324 if let Err(e) = result {
326 let error_msg = format!("{:?}", e);
327 assert!(
328 !error_msg.contains("parsing connection string"),
329 "Should not be a parsing error: {}",
330 error_msg
331 );
332 }
333 }
334
335 #[tokio::test]
336 async fn test_create_pool_with_unix_socket_path() {
337 let url = "postgresql:///postgres?host=/var/run/postgresql";
339 let result = create_pool(url, "test").await;
340 match result {
343 Ok(_) => {
344 }
346 Err(e) => {
347 let error_msg = format!("{:?}", e);
348 assert!(
350 !error_msg.contains("parsing connection string"),
351 "Should not be a parsing error: {}",
352 error_msg
353 );
354 }
355 }
356 }
357
358 #[tokio::test]
359 async fn test_create_pool_with_encoded_unix_socket() {
360 let url = "postgresql://%2Fvar%2Frun%2Fpostgresql/postgres";
362 let result = create_pool(url, "test").await;
363 match result {
365 Ok(_) => {
366 }
368 Err(e) => {
369 let error_msg = format!("{:?}", e);
370 assert!(
372 !error_msg.contains("parsing connection string"),
373 "Should not be a parsing error: {}",
374 error_msg
375 );
376 }
377 }
378 }
379
380 #[tokio::test]
381 async fn test_create_pool_with_no_host() {
382 let url = "postgresql:///postgres";
384 let result = create_pool(url, "test").await;
385 match result {
387 Ok(_) => {
388 }
390 Err(e) => {
391 let error_msg = format!("{:?}", e);
392 assert!(
394 !error_msg.contains("parsing connection string"),
395 "Should not be a parsing error: {}",
396 error_msg
397 );
398 }
399 }
400 }
401
402 #[tokio::test]
403 async fn test_unix_socket_connection_end_to_end() {
404 let url = "postgresql:///postgres?host=/var/run/postgresql";
406 let result = create_pool(url, "test").await;
407
408 match result {
409 Ok(pool) => {
410 let conn = pool.get().await;
412 if let Ok(conn) = conn {
413 let result = conn.simple_query("SELECT 1 as test").await;
414 assert!(result.is_ok(), "Query should succeed");
415 }
416 }
417 Err(e) => {
418 let error_msg = format!("{:?}", e);
419 assert!(
421 !error_msg.contains("parsing connection string"),
422 "Should not be a parsing error: {}",
423 error_msg
424 );
425 assert!(
426 !error_msg.contains("TLS handshake"),
427 "Should not be a TLS error for Unix socket: {}",
428 error_msg
429 );
430 }
431 }
432 }
433}