1use crate::driver::{
4 AuthSettings, GssEncMode, GssTokenProvider, GssTokenProviderEx, PgError, PgResult,
5 ScramChannelBindingMode, TlsConfig, TlsMode,
6};
7use std::time::Duration;
8
9#[derive(Clone)]
22pub struct PoolConfig {
23 pub host: String,
25 pub port: u16,
27 pub user: String,
29 pub database: String,
31 pub password: Option<String>,
33 pub max_connections: usize,
35 pub min_connections: usize,
37 pub idle_timeout: Duration,
39 pub acquire_timeout: Duration,
41 pub connect_timeout: Duration,
43 pub max_lifetime: Option<Duration>,
45 pub leaked_cleanup_queue: usize,
50 pub test_on_acquire: bool,
52 pub tls_mode: TlsMode,
54 pub tls_ca_cert_pem: Option<Vec<u8>>,
56 pub mtls: Option<TlsConfig>,
58 pub gss_token_provider: Option<GssTokenProvider>,
60 pub gss_token_provider_ex: Option<GssTokenProviderEx>,
62 pub gss_connect_retries: usize,
64 pub gss_retry_base_delay: Duration,
66 pub gss_circuit_breaker_threshold: usize,
68 pub gss_circuit_breaker_window: Duration,
70 pub gss_circuit_breaker_cooldown: Duration,
72 pub auth_settings: AuthSettings,
74 pub gss_enc_mode: GssEncMode,
76 pub io_uring: bool,
81}
82
83impl PoolConfig {
84 pub fn new(host: &str, port: u16, user: &str, database: &str) -> Self {
96 Self {
97 host: host.to_string(),
98 port,
99 user: user.to_string(),
100 database: database.to_string(),
101 password: None,
102 max_connections: 10,
103 min_connections: 1,
104 idle_timeout: Duration::from_secs(600), acquire_timeout: Duration::from_secs(30), connect_timeout: Duration::from_secs(10), max_lifetime: None, leaked_cleanup_queue: 64, test_on_acquire: false, tls_mode: TlsMode::Prefer,
111 tls_ca_cert_pem: None,
112 mtls: None,
113 gss_token_provider: None,
114 gss_token_provider_ex: None,
115 gss_connect_retries: 2,
116 gss_retry_base_delay: Duration::from_millis(150),
117 gss_circuit_breaker_threshold: 8,
118 gss_circuit_breaker_window: Duration::from_secs(30),
119 gss_circuit_breaker_cooldown: Duration::from_secs(15),
120 auth_settings: AuthSettings::scram_only(),
121 gss_enc_mode: GssEncMode::Disable,
122 io_uring: false,
123 }
124 }
125
126 pub fn new_dev(host: &str, port: u16, user: &str, database: &str) -> Self {
131 let mut config = Self::new(host, port, user, database);
132 config.tls_mode = TlsMode::Disable;
133 config.auth_settings = AuthSettings::default();
134 config
135 }
136
137 pub fn password(mut self, password: &str) -> Self {
139 self.password = Some(password.to_string());
140 self
141 }
142
143 pub fn max_connections(mut self, max: usize) -> Self {
145 self.max_connections = max;
146 self
147 }
148
149 pub fn min_connections(mut self, min: usize) -> Self {
151 self.min_connections = min;
152 self
153 }
154
155 pub fn idle_timeout(mut self, timeout: Duration) -> Self {
157 self.idle_timeout = timeout;
158 self
159 }
160
161 pub fn acquire_timeout(mut self, timeout: Duration) -> Self {
163 self.acquire_timeout = timeout;
164 self
165 }
166
167 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
169 self.connect_timeout = timeout;
170 self
171 }
172
173 pub fn max_lifetime(mut self, lifetime: Duration) -> Self {
175 self.max_lifetime = Some(lifetime);
176 self
177 }
178
179 pub fn leaked_cleanup_queue(mut self, max: usize) -> Self {
183 self.leaked_cleanup_queue = max;
184 self
185 }
186
187 pub fn test_on_acquire(mut self, enabled: bool) -> Self {
189 self.test_on_acquire = enabled;
190 self
191 }
192
193 pub fn tls_mode(mut self, mode: TlsMode) -> Self {
195 self.tls_mode = mode;
196 self
197 }
198
199 pub fn tls_ca_cert_pem(mut self, ca_cert_pem: Vec<u8>) -> Self {
201 self.tls_ca_cert_pem = Some(ca_cert_pem);
202 self
203 }
204
205 pub fn mtls(mut self, config: TlsConfig) -> Self {
207 self.mtls = Some(config);
208 self.tls_mode = TlsMode::Require;
209 self
210 }
211
212 pub fn gss_token_provider(mut self, provider: GssTokenProvider) -> Self {
214 self.gss_token_provider = Some(provider);
215 self
216 }
217
218 pub fn gss_token_provider_ex(mut self, provider: GssTokenProviderEx) -> Self {
220 self.gss_token_provider_ex = Some(provider);
221 self
222 }
223
224 pub fn gss_connect_retries(mut self, retries: usize) -> Self {
226 self.gss_connect_retries = retries;
227 self
228 }
229
230 pub fn gss_retry_base_delay(mut self, delay: Duration) -> Self {
232 self.gss_retry_base_delay = delay;
233 self
234 }
235
236 pub fn gss_circuit_breaker_threshold(mut self, threshold: usize) -> Self {
238 self.gss_circuit_breaker_threshold = threshold;
239 self
240 }
241
242 pub fn gss_circuit_breaker_window(mut self, window: Duration) -> Self {
244 self.gss_circuit_breaker_window = window;
245 self
246 }
247
248 pub fn gss_circuit_breaker_cooldown(mut self, cooldown: Duration) -> Self {
250 self.gss_circuit_breaker_cooldown = cooldown;
251 self
252 }
253
254 pub fn auth_settings(mut self, settings: AuthSettings) -> Self {
256 self.auth_settings = settings;
257 self
258 }
259
260 pub fn io_uring(mut self, enabled: bool) -> Self {
265 self.io_uring = enabled;
266 self
267 }
268
269 pub fn from_qail_config(qail: &qail_core::config::QailConfig) -> PgResult<Self> {
274 let pg = &qail.postgres;
275 let (host, port, user, database, password) = parse_pg_url(&pg.url)?;
276
277 let mut config = PoolConfig::new(&host, port, &user, &database)
278 .max_connections(pg.max_connections)
279 .min_connections(pg.min_connections)
280 .idle_timeout(Duration::from_secs(pg.idle_timeout_secs))
281 .acquire_timeout(Duration::from_secs(pg.acquire_timeout_secs))
282 .connect_timeout(Duration::from_secs(pg.connect_timeout_secs))
283 .test_on_acquire(pg.test_on_acquire)
284 .io_uring(pg.io_uring);
285
286 if let Some(ref pw) = password {
287 config = config.password(pw);
288 }
289
290 if let Some((_, query)) = pg.url.split_once('?') {
292 apply_url_query_params(&mut config, query, &host)?;
293 }
294
295 Ok(config)
296 }
297
298 pub fn from_url(url: &str) -> PgResult<Self> {
304 let (host, port, user, database, password) = parse_pg_url(url)?;
305 let mut config = PoolConfig::new(&host, port, &user, &database);
306
307 if let Some(ref pw) = password {
308 config = config.password(pw);
309 }
310
311 if let Some((_, query)) = url.split_once('?') {
312 apply_url_query_params(&mut config, query, &host)?;
313 }
314
315 Ok(config)
316 }
317}
318
319#[allow(unused_variables)]
324pub(crate) fn apply_url_query_params(
325 config: &mut PoolConfig,
326 query: &str,
327 host: &str,
328) -> PgResult<()> {
329 let mut sslcert: Option<String> = None;
330 let mut sslkey: Option<String> = None;
331 let mut gss_provider: Option<String> = None;
332 let mut gss_service = "postgres".to_string();
333 let mut gss_target: Option<String> = None;
334
335 for pair in query.split('&').filter(|p| !p.is_empty()) {
336 let mut kv = pair.splitn(2, '=');
337 let key = percent_decode(kv.next().unwrap_or_default().trim())?;
338 let value = percent_decode(kv.next().unwrap_or_default().trim())?;
339
340 match key.as_str() {
341 "sslmode" => {
342 let mode = TlsMode::parse_sslmode(&value).ok_or_else(|| {
343 PgError::Connection(format!("Invalid sslmode value: {}", value))
344 })?;
345 config.tls_mode = mode;
346 }
347 "gssencmode" => {
348 let mode = GssEncMode::parse_gssencmode(&value).ok_or_else(|| {
349 PgError::Connection(format!("Invalid gssencmode value: {}", value))
350 })?;
351 config.gss_enc_mode = mode;
352 }
353 "io_uring" => {
354 let enabled = parse_bool_param(&value).ok_or_else(|| {
355 PgError::Connection(format!("Invalid io_uring value: {}", value))
356 })?;
357 config.io_uring = enabled;
358 }
359 "sslrootcert" => {
360 let ca_pem = std::fs::read(&value).map_err(|e| {
361 PgError::Connection(format!("Failed to read sslrootcert '{}': {}", value, e))
362 })?;
363 config.tls_ca_cert_pem = Some(ca_pem);
364 }
365 "sslcert" => sslcert = Some(value.clone()),
366 "sslkey" => sslkey = Some(value.clone()),
367 "channel_binding" => {
368 let mode = ScramChannelBindingMode::parse(&value).ok_or_else(|| {
369 PgError::Connection(format!("Invalid channel_binding value: {}", value))
370 })?;
371 config.auth_settings.channel_binding = mode;
372 }
373 "auth_scram" => {
374 let enabled = parse_bool_param(&value).ok_or_else(|| {
375 PgError::Connection(format!("Invalid auth_scram value: {}", value))
376 })?;
377 config.auth_settings.allow_scram_sha_256 = enabled;
378 }
379 "auth_md5" => {
380 let enabled = parse_bool_param(&value).ok_or_else(|| {
381 PgError::Connection(format!("Invalid auth_md5 value: {}", value))
382 })?;
383 config.auth_settings.allow_md5_password = enabled;
384 }
385 "auth_cleartext" => {
386 let enabled = parse_bool_param(&value).ok_or_else(|| {
387 PgError::Connection(format!("Invalid auth_cleartext value: {}", value))
388 })?;
389 config.auth_settings.allow_cleartext_password = enabled;
390 }
391 "auth_kerberos" => {
392 let enabled = parse_bool_param(&value).ok_or_else(|| {
393 PgError::Connection(format!("Invalid auth_kerberos value: {}", value))
394 })?;
395 config.auth_settings.allow_kerberos_v5 = enabled;
396 }
397 "auth_gssapi" => {
398 let enabled = parse_bool_param(&value).ok_or_else(|| {
399 PgError::Connection(format!("Invalid auth_gssapi value: {}", value))
400 })?;
401 config.auth_settings.allow_gssapi = enabled;
402 }
403 "auth_sspi" => {
404 let enabled = parse_bool_param(&value).ok_or_else(|| {
405 PgError::Connection(format!("Invalid auth_sspi value: {}", value))
406 })?;
407 config.auth_settings.allow_sspi = enabled;
408 }
409 "auth_mode" => {
410 if value.eq_ignore_ascii_case("scram_only") {
411 config.auth_settings = AuthSettings::scram_only();
412 } else if value.eq_ignore_ascii_case("gssapi_only") {
413 config.auth_settings = AuthSettings::gssapi_only();
414 } else if value.eq_ignore_ascii_case("compat")
415 || value.eq_ignore_ascii_case("default")
416 {
417 config.auth_settings = AuthSettings::default();
418 } else {
419 return Err(PgError::Connection(format!(
420 "Invalid auth_mode value: {}",
421 value
422 )));
423 }
424 }
425 "gss_provider" => gss_provider = Some(value.clone()),
426 "gss_service" => {
427 if value.is_empty() {
428 return Err(PgError::Connection(
429 "gss_service must not be empty".to_string(),
430 ));
431 }
432 gss_service = value.clone();
433 }
434 "krbsrvname" => {
436 if value.is_empty() {
437 return Err(PgError::Connection(
438 "gss_service must not be empty".to_string(),
439 ));
440 }
441 gss_service = value.clone();
442 }
443 "gss_target" => {
444 if value.is_empty() {
445 return Err(PgError::Connection(
446 "gss_target must not be empty".to_string(),
447 ));
448 }
449 gss_target = Some(value.clone());
450 }
451 "gsshostname" => {
453 if value.is_empty() {
454 return Err(PgError::Connection(
455 "gss_target must not be empty".to_string(),
456 ));
457 }
458 gss_target = Some(value.clone());
459 }
460 "gsslib" => match value.trim().to_ascii_lowercase().as_str() {
463 "gssapi" | "sspi" => {}
464 _ => {
465 return Err(PgError::Connection(format!(
466 "Invalid gsslib value: {} (expected gssapi or sspi)",
467 value
468 )));
469 }
470 },
471 "gss_connect_retries" => {
472 let retries = value.parse::<usize>().map_err(|_| {
473 PgError::Connection(format!("Invalid gss_connect_retries value: {}", value))
474 })?;
475 if retries > 20 {
476 return Err(PgError::Connection(
477 "gss_connect_retries must be <= 20".to_string(),
478 ));
479 }
480 config.gss_connect_retries = retries;
481 }
482 "gss_retry_base_ms" => {
483 let delay_ms = value.parse::<u64>().map_err(|_| {
484 PgError::Connection(format!("Invalid gss_retry_base_ms value: {}", value))
485 })?;
486 if delay_ms == 0 {
487 return Err(PgError::Connection(
488 "gss_retry_base_ms must be greater than 0".to_string(),
489 ));
490 }
491 config.gss_retry_base_delay = Duration::from_millis(delay_ms);
492 }
493 "gss_circuit_threshold" => {
494 let threshold = value.parse::<usize>().map_err(|_| {
495 PgError::Connection(format!("Invalid gss_circuit_threshold value: {}", value))
496 })?;
497 if threshold > 100 {
498 return Err(PgError::Connection(
499 "gss_circuit_threshold must be <= 100".to_string(),
500 ));
501 }
502 config.gss_circuit_breaker_threshold = threshold;
503 }
504 "gss_circuit_window_ms" => {
505 let window_ms = value.parse::<u64>().map_err(|_| {
506 PgError::Connection(format!("Invalid gss_circuit_window_ms value: {}", value))
507 })?;
508 if window_ms == 0 {
509 return Err(PgError::Connection(
510 "gss_circuit_window_ms must be greater than 0".to_string(),
511 ));
512 }
513 config.gss_circuit_breaker_window = Duration::from_millis(window_ms);
514 }
515 "gss_circuit_cooldown_ms" => {
516 let cooldown_ms = value.parse::<u64>().map_err(|_| {
517 PgError::Connection(format!("Invalid gss_circuit_cooldown_ms value: {}", value))
518 })?;
519 if cooldown_ms == 0 {
520 return Err(PgError::Connection(
521 "gss_circuit_cooldown_ms must be greater than 0".to_string(),
522 ));
523 }
524 config.gss_circuit_breaker_cooldown = Duration::from_millis(cooldown_ms);
525 }
526 _ => {}
527 }
528 }
529
530 match (sslcert.as_deref(), sslkey.as_deref()) {
531 (Some(cert_path), Some(key_path)) => {
532 let mtls = TlsConfig {
533 client_cert_pem: std::fs::read(cert_path).map_err(|e| {
534 PgError::Connection(format!("Failed to read sslcert '{}': {}", cert_path, e))
535 })?,
536 client_key_pem: std::fs::read(key_path).map_err(|e| {
537 PgError::Connection(format!("Failed to read sslkey '{}': {}", key_path, e))
538 })?,
539 ca_cert_pem: config.tls_ca_cert_pem.clone(),
540 };
541 config.mtls = Some(mtls);
542 config.tls_mode = TlsMode::Require;
543 }
544 (Some(_), None) | (None, Some(_)) => {
545 return Err(PgError::Connection(
546 "Both sslcert and sslkey must be provided together".to_string(),
547 ));
548 }
549 (None, None) => {}
550 }
551
552 if let Some(provider) = gss_provider {
553 if provider.eq_ignore_ascii_case("linux_krb5") || provider.eq_ignore_ascii_case("builtin") {
554 #[cfg(all(feature = "enterprise-gssapi", target_os = "linux"))]
555 {
556 let provider = crate::driver::gss::linux_krb5_token_provider(
557 crate::driver::gss::LinuxKrb5ProviderConfig {
558 host: host.to_string(),
559 service: gss_service,
560 target_name: gss_target,
561 },
562 )
563 .map_err(PgError::Auth)?;
564 config.gss_token_provider_ex = Some(provider);
565 }
566 #[cfg(not(all(feature = "enterprise-gssapi", target_os = "linux")))]
567 {
568 let _ = gss_service;
569 let _ = gss_target;
570 return Err(PgError::Connection(
571 "gss_provider=linux_krb5 requires qail-pg feature enterprise-gssapi on Linux"
572 .to_string(),
573 ));
574 }
575 } else if provider.eq_ignore_ascii_case("callback")
576 || provider.eq_ignore_ascii_case("custom")
577 {
578 } else {
580 return Err(PgError::Connection(format!(
581 "Invalid gss_provider value: {}",
582 provider
583 )));
584 }
585 }
586
587 Ok(())
588}
589
590pub(super) fn parse_pg_url(url: &str) -> PgResult<(String, u16, String, String, Option<String>)> {
592 let url = url.split('?').next().unwrap_or(url);
593 let url = if let Some(rest) = url.strip_prefix("postgres://") {
594 rest
595 } else if let Some(rest) = url.strip_prefix("postgresql://") {
596 rest
597 } else {
598 return Err(PgError::Connection(
599 "PostgreSQL URL must start with postgres:// or postgresql://".to_string(),
600 ));
601 };
602
603 let (credentials, host_part) = if let Some(at_pos) = url.rfind('@') {
604 let creds = &url[..at_pos];
605 let host = &url[at_pos + 1..];
606 (Some(creds), host)
607 } else {
608 (None, url)
609 };
610
611 let (host_port, database) = if host_part.contains('/') {
612 let mut parts = host_part.splitn(2, '/');
613 let host_port = parts.next().unwrap_or("localhost");
614 let database = percent_decode(parts.next().unwrap_or("postgres"))?;
615 if database.is_empty() {
616 return Err(PgError::Connection(
617 "Invalid PostgreSQL URL database: missing database name".to_string(),
618 ));
619 }
620 (host_port, database)
621 } else {
622 (host_part, "postgres".to_string())
623 };
624
625 let (host, port) = if host_port.starts_with('[') {
626 let end = host_port.find(']').ok_or_else(|| {
627 PgError::Connection("Invalid PostgreSQL URL IPv6 host: missing ']'".to_string())
628 })?;
629 let host = &host_port[..=end];
630 if host == "[]" {
631 return Err(PgError::Connection(
632 "Invalid PostgreSQL URL host: missing host".to_string(),
633 ));
634 }
635 let suffix = &host_port[end + 1..];
636 let port = if suffix.is_empty() {
637 5432u16
638 } else if let Some(port_str) = suffix.strip_prefix(':') {
639 if port_str.is_empty() {
640 return Err(PgError::Connection(
641 "Invalid PostgreSQL URL port: missing port after ':'".to_string(),
642 ));
643 }
644 let p = port_str.parse::<u16>().map_err(|_| {
645 PgError::Connection(format!(
646 "Invalid PostgreSQL URL port '{}': expected a number from 1 to 65535",
647 port_str
648 ))
649 })?;
650 if p == 0 {
651 return Err(PgError::Connection(
652 "Invalid PostgreSQL URL port '0': expected a number from 1 to 65535"
653 .to_string(),
654 ));
655 }
656 p
657 } else {
658 return Err(PgError::Connection(
659 "Invalid PostgreSQL URL IPv6 host: unexpected characters after ']'".to_string(),
660 ));
661 };
662 (host.to_string(), port)
663 } else if host_port.contains(':') {
664 let mut parts = host_port.splitn(2, ':');
665 let h = parts.next().unwrap_or("localhost").to_string();
666 if h.is_empty() {
667 return Err(PgError::Connection(
668 "Invalid PostgreSQL URL host: missing host".to_string(),
669 ));
670 }
671 let port_str = parts.next().unwrap_or("");
672 if port_str.is_empty() {
673 return Err(PgError::Connection(
674 "Invalid PostgreSQL URL port: missing port after ':'".to_string(),
675 ));
676 }
677 let p = port_str.parse::<u16>().map_err(|_| {
678 PgError::Connection(format!(
679 "Invalid PostgreSQL URL port '{}': expected a number from 1 to 65535",
680 port_str
681 ))
682 })?;
683 if p == 0 {
684 return Err(PgError::Connection(
685 "Invalid PostgreSQL URL port '0': expected a number from 1 to 65535".to_string(),
686 ));
687 }
688 (h, p)
689 } else {
690 if host_port.is_empty() {
691 return Err(PgError::Connection(
692 "Invalid PostgreSQL URL host: missing host".to_string(),
693 ));
694 }
695 (host_port.to_string(), 5432u16)
696 };
697
698 let (user, password) = if let Some(creds) = credentials {
699 if creds.contains(':') {
700 let mut parts = creds.splitn(2, ':');
701 let u = percent_decode(parts.next().unwrap_or("postgres"))?;
702 if u.is_empty() {
703 return Err(PgError::Connection(
704 "Invalid PostgreSQL URL user: missing user".to_string(),
705 ));
706 }
707 let p = parts.next().map(percent_decode).transpose()?;
708 (u, p)
709 } else {
710 let u = percent_decode(creds)?;
711 if u.is_empty() {
712 return Err(PgError::Connection(
713 "Invalid PostgreSQL URL user: missing user".to_string(),
714 ));
715 }
716 (u, None)
717 }
718 } else {
719 ("postgres".to_string(), None)
720 };
721
722 Ok((host, port, user, database, password))
723}
724
725fn percent_decode(s: &str) -> PgResult<String> {
726 fn hex_value(byte: u8) -> Option<u8> {
727 match byte {
728 b'0'..=b'9' => Some(byte - b'0'),
729 b'a'..=b'f' => Some(byte - b'a' + 10),
730 b'A'..=b'F' => Some(byte - b'A' + 10),
731 _ => None,
732 }
733 }
734
735 let bytes = s.as_bytes();
736 let mut decoded = Vec::with_capacity(bytes.len());
737 let mut i = 0;
738
739 while i < bytes.len() {
740 if bytes[i] == b'%' {
741 if i + 2 >= bytes.len() {
742 return Err(PgError::Connection(
743 "Invalid PostgreSQL URL percent-encoding: '%' must be followed by two hex digits"
744 .to_string(),
745 ));
746 }
747 let (Some(hi), Some(lo)) = (hex_value(bytes[i + 1]), hex_value(bytes[i + 2])) else {
748 return Err(PgError::Connection(
749 "Invalid PostgreSQL URL percent-encoding: '%' must be followed by two hex digits"
750 .to_string(),
751 ));
752 };
753 decoded.push((hi << 4) | lo);
754 i += 3;
755 } else {
756 decoded.push(bytes[i]);
757 i += 1;
758 }
759 }
760
761 String::from_utf8(decoded).map_err(|_| {
762 PgError::Connection(
763 "Invalid PostgreSQL URL percent-encoding: decoded value is not UTF-8".to_string(),
764 )
765 })
766}
767
768pub(super) fn parse_bool_param(value: &str) -> Option<bool> {
769 match value.trim().to_ascii_lowercase().as_str() {
770 "1" | "true" | "yes" | "on" => Some(true),
771 "0" | "false" | "no" | "off" => Some(false),
772 _ => None,
773 }
774}