1#[derive(Clone)]
4pub struct DatabaseConfig {
5 pub url: String,
6 pub max_connections: u32,
7 pub min_connections: u32,
8 pub connect_timeout: u64,
9 pub idle_timeout: u64,
10 pub max_lifetime: u64,
12 pub test_before_acquire: bool,
15 pub statement_timeout_ms: Option<u64>,
17 pub lock_timeout_ms: Option<u64>,
19 pub slow_query_threshold_ms: u64,
21 pub after_connect: Vec<String>,
24}
25
26impl std::fmt::Debug for DatabaseConfig {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 f.debug_struct("DatabaseConfig")
29 .field("url", &Self::redact_url(&self.url))
30 .field("max_connections", &self.max_connections)
31 .field("min_connections", &self.min_connections)
32 .field("connect_timeout", &self.connect_timeout)
33 .field("idle_timeout", &self.idle_timeout)
34 .field("max_lifetime", &self.max_lifetime)
35 .field("test_before_acquire", &self.test_before_acquire)
36 .field("statement_timeout_ms", &self.statement_timeout_ms)
37 .field("lock_timeout_ms", &self.lock_timeout_ms)
38 .field("slow_query_threshold_ms", &self.slow_query_threshold_ms)
39 .field("after_connect", &self.after_connect)
40 .finish()
41 }
42}
43
44impl DatabaseConfig {
45 pub fn redact_url(url: &str) -> String {
46 let scheme_end = match url.find("://") {
47 Some(idx) => idx,
48 None => return url.to_string(),
49 };
50 let after_scheme = &url[scheme_end + 3..];
51 let authority_end = after_scheme
53 .find(['/', '?', '#'])
54 .unwrap_or(after_scheme.len());
55 let at_idx = match after_scheme[..authority_end].rfind('@') {
56 Some(idx) => idx,
57 None => return url.to_string(),
58 };
59 let authority = &after_scheme[..at_idx];
60 let colon_idx = match authority.find(':') {
61 Some(idx) => idx,
62 None => return url.to_string(),
63 };
64 let user = &authority[..colon_idx];
65 format!(
66 "{}://{}:***@{}",
67 &url[..scheme_end],
68 user,
69 &after_scheme[at_idx + 1..]
70 )
71 }
72
73 pub fn from_url(url: impl Into<String>) -> Self {
79 Self {
80 url: url.into(),
81 max_connections: 20,
82 min_connections: 2,
83 connect_timeout: 10,
84 idle_timeout: 600,
85 max_lifetime: 1800,
86 test_before_acquire: false,
87 statement_timeout_ms: None,
88 lock_timeout_ms: None,
89 slow_query_threshold_ms: 500,
90 after_connect: Vec::new(),
91 }
92 }
93
94 pub fn from_env() -> Result<Self, std::env::VarError> {
96 Self::from_env_var("DATABASE_URL")
97 }
98
99 pub fn from_env_var(var: &str) -> Result<Self, std::env::VarError> {
100 let url = std::env::var(var)?;
101 Ok(Self::from_url(url))
102 }
103
104 pub fn backend(&self) -> BackendKind {
106 if self.url.starts_with("postgres") {
107 BackendKind::Postgres
108 } else if self.url.starts_with("mysql") || self.url.starts_with("mariadb") {
109 BackendKind::Mysql
110 } else if self.url.starts_with("sqlite") {
111 BackendKind::Sqlite
112 } else {
113 BackendKind::Unknown
114 }
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum BackendKind {
120 Postgres,
121 Mysql,
122 Sqlite,
123 Unknown,
124}
125
126impl BackendKind {
127 pub fn quote_ident(self, ident: &str) -> String {
129 match self {
130 BackendKind::Mysql => crate::query::quote_ident_mysql(ident),
131 _ => crate::query::quote_ident(ident),
132 }
133 }
134}
135
136#[doc(hidden)]
138pub fn is_local_host(host: &str) -> bool {
139 host.starts_with('/') || matches!(host, "localhost" | "127.0.0.1" | "::1" | "[::1]")
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn local_hosts_are_not_forced_to_tls() {
148 for h in ["localhost", "127.0.0.1", "::1", "/var/run/postgresql"] {
149 assert!(is_local_host(h), "{h}");
150 }
151 for h in ["db.example.com", "10.0.0.5", "localhost.evil.com"] {
152 assert!(!is_local_host(h), "{h}");
153 }
154 }
155
156 #[test]
157 fn redact_url_replaces_password() {
158 assert_eq!(
159 DatabaseConfig::redact_url("postgres://user:pass@host:5432/db"),
160 "postgres://user:***@host:5432/db"
161 );
162 }
163
164 #[test]
165 fn redact_url_handles_special_chars_and_query() {
166 assert_eq!(
167 DatabaseConfig::redact_url("postgres://u:p@ss:w@host/db"),
168 "postgres://u:***@host/db"
169 );
170 assert_eq!(
171 DatabaseConfig::redact_url("postgres://host:5432/db?x=a@b"),
172 "postgres://host:5432/db?x=a@b"
173 );
174 }
175
176 #[test]
177 fn redact_url_without_password_is_unchanged() {
178 assert_eq!(
179 DatabaseConfig::redact_url("postgres://user@host:5432/db"),
180 "postgres://user@host:5432/db"
181 );
182 }
183
184 #[test]
185 fn redact_url_sqlite_memory_is_unchanged() {
186 assert_eq!(
187 DatabaseConfig::redact_url("sqlite::memory:"),
188 "sqlite::memory:"
189 );
190 }
191
192 #[test]
193 fn database_config_debug_redacts_url() {
194 let config = DatabaseConfig::from_url("postgres://user:secret@host:5432/db");
195 let debug = format!("{:?}", config);
196 assert!(!debug.contains("secret"));
197 assert!(debug.contains("user:***@host"));
198 }
199
200 #[test]
201 fn from_url_defaults() {
202 let config = DatabaseConfig::from_url("postgres://user:pass@host:5432/db");
203 assert_eq!(config.max_lifetime, 1800);
204 assert!(!config.test_before_acquire);
205 assert_eq!(config.statement_timeout_ms, None);
206 assert_eq!(config.lock_timeout_ms, None);
207 assert_eq!(config.slow_query_threshold_ms, 500);
208 }
209}