rws_server/model/connection.rs
1//! Database connection configuration.
2
3use super::{Backend, DbError};
4
5/// Database connection configuration.
6///
7/// For SQLite, only `database` matters (the file path or `":memory:"`).
8/// For PostgreSQL and MySQL, `host`, `port`, `user`, `password`, and `database` are all used.
9#[derive(Debug, Clone)]
10pub struct DbConfig {
11 /// Hostname (ignored for SQLite).
12 pub host: String,
13 /// Port (ignored for SQLite). PostgreSQL default: 5432. MySQL default: 3306.
14 pub port: u16,
15 /// Username (ignored for SQLite).
16 pub user: String,
17 /// Password (ignored for SQLite).
18 pub password: String,
19 /// Database name. For SQLite, this is the file path.
20 pub database: String,
21 /// Maximum number of connections in the pool.
22 pub pool_size: u32,
23 /// Which compiled-in backend this config connects to.
24 ///
25 /// Only needs to be set explicitly when more than one `model-*` feature
26 /// is compiled into the binary — see [`Backend`]. When only one is
27 /// compiled in, [`DbConfig::from_env`] infers it automatically.
28 pub backend: Backend,
29}
30
31impl DbConfig {
32 /// Read configuration from environment variables.
33 ///
34 /// | Variable | Default |
35 /// |---|---|
36 /// | `RWS_DB_BACKEND` | Inferred if exactly one `model-*` feature is compiled in; **required** otherwise |
37 /// | `RWS_DB_HOST` | `localhost` |
38 /// | `RWS_DB_PORT` | `5432` |
39 /// | `RWS_DB_USER` | _(empty)_ |
40 /// | `RWS_DB_PASSWORD` | _(empty)_ |
41 /// | `RWS_DB_NAME` | **(required)** |
42 /// | `RWS_DB_POOL_SIZE` | `10` |
43 pub fn from_env() -> Result<Self, DbError> {
44 let backend = match std::env::var("RWS_DB_BACKEND") {
45 Ok(s) => Backend::parse(&s)?,
46 Err(_) => Backend::unambiguous_default().ok_or_else(|| {
47 DbError::new(
48 "RWS_DB_BACKEND must be set to \"sqlite\", \"postgres\", or \"mysql\" \
49 when more than one model-* feature is compiled in",
50 )
51 })?,
52 };
53 let host = std::env::var("RWS_DB_HOST").unwrap_or_else(|_| "localhost".into());
54 let port = std::env::var("RWS_DB_PORT")
55 .unwrap_or_else(|_| "5432".into())
56 .parse::<u16>()
57 .map_err(|e| DbError::new(format!("RWS_DB_PORT: {}", e)))?;
58 let user = std::env::var("RWS_DB_USER").unwrap_or_default();
59 let password = std::env::var("RWS_DB_PASSWORD").unwrap_or_default();
60 let database = std::env::var("RWS_DB_NAME")
61 .map_err(|_| DbError::new("RWS_DB_NAME environment variable is required"))?;
62 let pool_size = std::env::var("RWS_DB_POOL_SIZE")
63 .unwrap_or_else(|_| "10".into())
64 .parse::<u32>()
65 .map_err(|e| DbError::new(format!("RWS_DB_POOL_SIZE: {}", e)))?;
66
67 Ok(DbConfig { host, port, user, password, database, pool_size, backend })
68 }
69
70 /// Connection URL suitable for passing to sqlx.
71 #[doc(hidden)]
72 pub fn to_url(&self) -> String {
73 match self.backend {
74 #[cfg(feature = "model-sqlite")]
75 Backend::Sqlite => format!("sqlite:{}", self.database),
76 #[cfg(feature = "model-postgres")]
77 Backend::Postgres => format!(
78 "postgres://{}:{}@{}:{}/{}",
79 self.user, self.password, self.host, self.port, self.database
80 ),
81 #[cfg(feature = "model-mysql")]
82 Backend::MySql => format!(
83 "mysql://{}:{}@{}:{}/{}",
84 self.user, self.password, self.host, self.port, self.database
85 ),
86 }
87 }
88}
89
90/// How a PostgreSQL or MySQL connection uses TLS (ignored for SQLite).
91///
92/// Separate from [`DbConfig`] so code that builds a `DbConfig` literal keeps
93/// compiling; [`DbPool::from_env`](super::DbPool::from_env) reads both, and
94/// [`DbPool::new_with_tls`](super::DbPool::new_with_tls) takes both.
95///
96/// | Variable | Default |
97/// |---|---|
98/// | `RWS_DB_SSL_MODE` | `prefer` |
99/// | `RWS_DB_SSL_ROOT_CERT` | _(none: the system's roots)_ |
100/// | `RWS_DB_SSL_CLIENT_CERT` | _(none)_ |
101/// | `RWS_DB_SSL_CLIENT_KEY` | _(none)_ |
102#[derive(Debug, Clone, Default, PartialEq)]
103pub struct DbTls {
104 /// When to use TLS and what to check.
105 pub mode: DbSslMode,
106 /// PEM file with the CA certificates that sign the server's certificate
107 /// (a managed database's private CA).
108 pub root_cert: Option<String>,
109 /// PEM client certificate, for servers that require one.
110 pub client_cert: Option<String>,
111 /// PEM private key of `client_cert`.
112 pub client_key: Option<String>,
113}
114
115/// TLS modes, named as libpq's `sslmode`.
116#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
117pub enum DbSslMode {
118 /// Never TLS.
119 Disable,
120 /// TLS when the server offers it, without checking its certificate
121 /// (the default, as before these settings existed).
122 #[default]
123 Prefer,
124 /// TLS always, certificate not checked.
125 Require,
126 /// TLS always, certificate signed by a trusted CA.
127 VerifyCa,
128 /// As `VerifyCa`, and the certificate names the host connected to.
129 VerifyFull,
130}
131
132impl DbSslMode {
133 /// Parse `disable`, `prefer`, `require`, `verify-ca` or `verify-full`
134 /// (MySQL's spellings `disabled`, `preferred`, `required`,
135 /// `verify_identity` too).
136 pub fn parse(value: &str) -> Result<DbSslMode, DbError> {
137 Ok(match value.trim().to_ascii_lowercase().replace('_', "-").as_str() {
138 "disable" | "disabled" => DbSslMode::Disable,
139 "" | "prefer" | "preferred" => DbSslMode::Prefer,
140 "require" | "required" => DbSslMode::Require,
141 "verify-ca" => DbSslMode::VerifyCa,
142 "verify-full" | "verify-identity" => DbSslMode::VerifyFull,
143 other => {
144 return Err(DbError::new(format!(
145 "RWS_DB_SSL_MODE: {other:?} is not disable, prefer, require, verify-ca or verify-full"
146 )))
147 }
148 })
149 }
150}
151
152impl DbTls {
153 /// Read `RWS_DB_SSL_*` (see the table above). A client certificate
154 /// without its key, or the other way round, is an error.
155 pub fn from_env() -> Result<DbTls, DbError> {
156 let get = |name: &str| std::env::var(name).ok().map(|v| v.trim().to_string()).filter(|v| !v.is_empty());
157 let tls = DbTls {
158 mode: DbSslMode::parse(&get("RWS_DB_SSL_MODE").unwrap_or_default())?,
159 root_cert: get("RWS_DB_SSL_ROOT_CERT"),
160 client_cert: get("RWS_DB_SSL_CLIENT_CERT"),
161 client_key: get("RWS_DB_SSL_CLIENT_KEY"),
162 };
163 if tls.client_cert.is_some() != tls.client_key.is_some() {
164 return Err(DbError::new("RWS_DB_SSL_CLIENT_CERT and RWS_DB_SSL_CLIENT_KEY go together"));
165 }
166 Ok(tls)
167 }
168}