Skip to main content

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}