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