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 let mut config = url::parse_connection_url(url)?;
100
101 config.application_name(application_name);
102
103 let mut tried_ssl_fallback = false;
104
105 let pool = loop {
107 debug!("Creating manager");
108 let tls = config.get_ssl_mode() != SslMode::Disable;
109 let manager = crate::pool::PgConnectionManager::new(config.clone(), tls);
110
111 debug!("Creating pool");
112 let pool = Pool::builder()
113 .max_lifetime(Some(Duration::from_secs(3600)))
114 .build(manager.clone());
115
116 let pool = PgPool {
117 manager,
118 inner: pool,
119 };
120
121 debug!("Checking pool");
122 match check_pool(&pool).await {
123 Ok(_) => {
124 if tried_ssl_fallback {
125 tracing::info!("Connected successfully with SSL disabled after TLS error");
126 }
127 break pool;
128 }
129 Err(e) => {
130 debug!("Connection error: {:#}", e);
131 debug!(
132 "is_tls_error: {}, is_auth_error: {}",
133 is_tls_error(&e),
134 is_auth_error(&e)
135 );
136
137 if is_tls_error(&e) {
138 if config.get_ssl_mode() == SslMode::Prefer && !tried_ssl_fallback {
140 debug!("TLS failed with prefer mode, retrying with SSL disabled");
141 config.ssl_mode(SslMode::Disable);
142 tried_ssl_fallback = true;
143 continue;
144 }
145
146 return Err(e).wrap_err(
148 "TLS/SSL connection failed. Try using --ssl disable, \
149 or use a connection URL with sslmode=disable: \
150 postgresql://user@host/db?sslmode=disable",
151 );
152 } else if is_auth_error(&e) && config.get_password().is_none() {
153 let password = rpassword::prompt_password("Password: ").into_diagnostic()?;
154 config.password(password);
155 } else {
157 return Err(e);
159 }
160 }
161 }
162 };
163
164 Ok(pool)
165}
166
167pub async fn connect_one(url: &str, application_name: &str) -> Result<tokio_postgres::Client> {
176 use mobc::Manager as _;
177
178 let pool = create_pool(url, application_name).await?;
179 pool.manager.connect().await.into_diagnostic()
180}
181
182async fn check_pool(pool: &PgPool) -> Result<()> {
184 let conn = match pool.get().await {
185 Err(mobc::Error::Inner(db_err)) => {
186 return Err(match db_err.as_db_error() {
187 Some(db_err) => miette!(
188 "E{code} at {func} in {file}:{line}",
189 code = db_err.code().code(),
190 func = db_err.routine().unwrap_or("{unknown}"),
191 file = db_err.file().unwrap_or("unknown.c"),
192 line = db_err.line().unwrap_or(0)
193 ),
194 _ => miette!("{db_err}"),
195 })
196 .wrap_err(
197 db_err
198 .as_db_error()
199 .map(|e| e.to_string())
200 .unwrap_or_default(),
201 )?;
202 }
203 res @ Err(_) => {
204 let res = res.map(drop).into_diagnostic();
205 return if let Err(ref err) = res
206 && is_auth_error(err)
207 {
208 res.wrap_err("hint: check the password")
209 } else {
210 res
211 };
212 }
213 Ok(conn) => conn,
214 };
215 conn.simple_query("SELECT 1")
216 .await
217 .into_diagnostic()
218 .wrap_err("checking connection")?;
219 Ok(())
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[tokio::test]
227 async fn test_create_pool_valid_connection_string() {
228 let connection_string = "postgresql://localhost/test";
229 let result = create_pool(connection_string, "test").await;
230 if let Err(e) = result {
232 let error_msg = format!("{:?}", e);
233 assert!(
234 !error_msg.contains("parsing connection string"),
235 "Should not be a parsing error: {}",
236 error_msg
237 );
238 }
239 }
240
241 #[tokio::test]
242 async fn test_create_pool_with_full_url() {
243 let connection_string = "postgresql://user:pass@localhost:5432/testdb";
244 let result = create_pool(connection_string, "test").await;
245 if let Err(e) = result {
247 let error_msg = format!("{:?}", e);
248 assert!(
249 !error_msg.contains("parsing connection string"),
250 "Should not be a parsing error: {}",
251 error_msg
252 );
253 }
254 }
255
256 #[tokio::test]
257 async fn test_create_pool_with_unix_socket_path() {
258 let url = "postgresql:///postgres?host=/var/run/postgresql";
260 let result = create_pool(url, "test").await;
261 match result {
264 Ok(_) => {
265 }
267 Err(e) => {
268 let error_msg = format!("{:?}", e);
269 assert!(
271 !error_msg.contains("parsing connection string"),
272 "Should not be a parsing error: {}",
273 error_msg
274 );
275 }
276 }
277 }
278
279 #[tokio::test]
280 async fn test_create_pool_with_encoded_unix_socket() {
281 let url = "postgresql://%2Fvar%2Frun%2Fpostgresql/postgres";
283 let result = create_pool(url, "test").await;
284 match result {
286 Ok(_) => {
287 }
289 Err(e) => {
290 let error_msg = format!("{:?}", e);
291 assert!(
293 !error_msg.contains("parsing connection string"),
294 "Should not be a parsing error: {}",
295 error_msg
296 );
297 }
298 }
299 }
300
301 #[tokio::test]
302 async fn test_create_pool_with_no_host() {
303 let url = "postgresql:///postgres";
305 let result = create_pool(url, "test").await;
306 match result {
308 Ok(_) => {
309 }
311 Err(e) => {
312 let error_msg = format!("{:?}", e);
313 assert!(
315 !error_msg.contains("parsing connection string"),
316 "Should not be a parsing error: {}",
317 error_msg
318 );
319 }
320 }
321 }
322
323 #[tokio::test]
324 async fn test_unix_socket_connection_end_to_end() {
325 let url = "postgresql:///postgres?host=/var/run/postgresql";
327 let result = create_pool(url, "test").await;
328
329 match result {
330 Ok(pool) => {
331 let conn = pool.get().await;
333 if let Ok(conn) = conn {
334 let result = conn.simple_query("SELECT 1 as test").await;
335 assert!(result.is_ok(), "Query should succeed");
336 }
337 }
338 Err(e) => {
339 let error_msg = format!("{:?}", e);
340 assert!(
342 !error_msg.contains("parsing connection string"),
343 "Should not be a parsing error: {}",
344 error_msg
345 );
346 assert!(
347 !error_msg.contains("TLS handshake"),
348 "Should not be a TLS error for Unix socket: {}",
349 error_msg
350 );
351 }
352 }
353 }
354}