sos_server/
config.rs

1//! Server configuration.
2use super::backend::Backend;
3use super::{Error, Result};
4use colored::Colorize;
5use serde::{Deserialize, Serialize};
6use sos_backend::BackendTarget;
7use sos_core::{AccountId, Paths};
8use sos_database::{migrations::migrate_client, open_file};
9use sos_vfs as vfs;
10use std::{
11    collections::HashSet,
12    net::{IpAddr, Ipv4Addr, SocketAddr},
13    path::{Path, PathBuf},
14};
15use url::Url;
16
17/// Configuration for the web server.
18#[derive(Default, Debug, Serialize, Deserialize)]
19#[serde(default)]
20pub struct ServerConfig {
21    /// Storage for the backend.
22    pub storage: StorageConfig,
23
24    /// Log configuration.
25    pub log: LogConfig,
26
27    /// Access controls.
28    pub access: Option<AccessControlConfig>,
29
30    /// Configuration for the network.
31    pub net: NetworkConfig,
32
33    /// Path the file was loaded from used to determine
34    /// relative paths.
35    #[serde(skip)]
36    file: Option<PathBuf>,
37}
38
39/// Access control configuration.
40///
41/// Denied entries take precedence so if you allow and
42/// deny the same address it will be denied.
43#[derive(Debug, Default, Clone, Serialize, Deserialize)]
44pub struct AccessControlConfig {
45    /// AccountIdes that are explicitly allowed.
46    pub allow: Option<HashSet<AccountId>>,
47    /// AccountIdes that are explicitly denied.
48    pub deny: Option<HashSet<AccountId>>,
49}
50
51impl AccessControlConfig {
52    /// Determine if a signing key address is allowed access
53    /// to this server.
54    pub fn is_allowed_access(&self, account_id: &AccountId) -> bool {
55        let has_definitions = self.allow.is_some() || self.deny.is_some();
56        if has_definitions {
57            match (&self.deny, &self.allow) {
58                (Some(deny), None) => {
59                    if deny.iter().any(|a| a == account_id) {
60                        return false;
61                    }
62                    true
63                }
64                (None, Some(allow)) => {
65                    if allow.iter().any(|a| a == account_id) {
66                        return true;
67                    }
68                    false
69                }
70                (Some(deny), Some(allow)) => {
71                    if allow.iter().any(|a| a == account_id) {
72                        return true;
73                    }
74                    if deny.iter().any(|a| a == account_id) {
75                        return false;
76                    }
77                    false
78                }
79                _ => true,
80            }
81        } else {
82            true
83        }
84    }
85}
86
87/// Log file configuration.
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct LogConfig {
90    /// Directory for log files.
91    pub directory: PathBuf,
92    /// Name of log files.
93    pub name: String,
94    /// Tracing level.
95    pub level: String,
96}
97
98impl Default for LogConfig {
99    fn default() -> Self {
100        Self {
101            directory: PathBuf::from("logs"),
102            name: "sos-server.log".to_string(),
103            level: "sos_server=info".to_string(),
104        }
105    }
106}
107
108/// Server network configuration.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(default)]
111pub struct NetworkConfig {
112    /// Bind address for the server.
113    pub bind: SocketAddr,
114
115    /// SSL configuration.
116    pub ssl: Option<SslConfig>,
117
118    /// Configuration for CORS.
119    pub cors: Option<CorsConfig>,
120}
121
122impl Default for NetworkConfig {
123    fn default() -> Self {
124        Self {
125            bind: SocketAddr::new(
126                IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
127                5053,
128            ),
129            ssl: Default::default(),
130            cors: None,
131        }
132    }
133}
134
135/// Server SSL configuration.
136#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(rename_all = "lowercase", untagged)]
138pub enum SslConfig {
139    /// Configuration for TLS certificate and private key.
140    Tls(TlsConfig),
141    /// Configuration for Let's Encrypt ACME certificates.
142    #[cfg(feature = "acme")]
143    Acme(AcmeConfig),
144}
145
146/// Certificate and key for TLS.
147#[derive(Debug, Default, Clone, Serialize, Deserialize)]
148pub struct TlsConfig {
149    /// Path to the certificate.
150    pub cert: PathBuf,
151    /// Path to the certificate key file.
152    pub key: PathBuf,
153}
154
155/// Configuration for ACME certficates.
156#[cfg(feature = "acme")]
157#[derive(Debug, Default, Clone, Serialize, Deserialize)]
158pub struct AcmeConfig {
159    /// Path to the cache directory.
160    pub cache: PathBuf,
161    /// List of domain names.
162    pub domains: Vec<String>,
163    /// List of email addresses.
164    pub email: Vec<String>,
165    /// Use production environment.
166    pub production: bool,
167}
168
169/// Configuration for CORS.
170#[derive(Debug, Clone, Default, Serialize, Deserialize)]
171pub struct CorsConfig {
172    /// List of additional CORS origins for the server.
173    pub origins: Vec<Url>,
174}
175
176/// Configuration for storage locations.
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct StorageConfig {
179    /// URL for the backend storage.
180    pub path: PathBuf,
181
182    /// Database file.
183    ///
184    /// When this field is given the server will use
185    /// the database backend.
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub database: Option<String>,
188
189    /// Parsed database URI.
190    #[serde(skip)]
191    pub database_uri: Option<UriOrPath>,
192}
193
194/// URI or path reference.
195#[derive(Debug, Clone)]
196pub enum UriOrPath {
197    /// URI reference.
198    Uri(http::Uri),
199    /// Path reference.
200    Path(PathBuf),
201}
202
203impl UriOrPath {
204    /// URI string representation.
205    pub fn as_uri_string(&self) -> String {
206        match self {
207            UriOrPath::Uri(uri) => uri.to_string(),
208            UriOrPath::Path(path) => format!("file:{}", path.display()),
209        }
210    }
211}
212
213impl StorageConfig {
214    /// Set the database URI.
215    #[doc(hidden)]
216    fn set_database_uri(
217        &mut self,
218        db: &str,
219        base_dir: impl AsRef<Path>,
220    ) -> Result<()> {
221        let uri = if db.starts_with("file:") {
222            UriOrPath::Uri(db.parse()?)
223        } else {
224            let path = PathBuf::from(db);
225            if path.is_relative() {
226                let path = base_dir.as_ref().join(path);
227                if !path.exists() {
228                    std::fs::File::create(&path)?;
229                }
230                UriOrPath::Path(path.canonicalize()?)
231            } else {
232                UriOrPath::Path(path)
233            }
234        };
235
236        self.database_uri = Some(uri);
237        Ok(())
238    }
239}
240
241impl Default for StorageConfig {
242    fn default() -> Self {
243        Self {
244            path: PathBuf::from("."),
245            database: None,
246            database_uri: None,
247        }
248    }
249}
250
251impl ServerConfig {
252    /// Load a server config from a file path.
253    pub async fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
254        if !vfs::try_exists(path.as_ref()).await? {
255            return Err(Error::NotFile(path.as_ref().to_path_buf()));
256        }
257
258        let contents = vfs::read_to_string(path.as_ref()).await?;
259        let mut config: ServerConfig = toml::from_str(&contents)?;
260        config.file = Some(path.as_ref().canonicalize()?);
261
262        let dir = config.directory();
263
264        if config.log.directory.is_relative() {
265            config.log.directory = dir.join(&config.log.directory);
266            if !config.log.directory.exists() {
267                vfs::create_dir_all(&config.log.directory).await?;
268            }
269            config.log.directory = config.log.directory.canonicalize()?;
270        }
271
272        if let Some(SslConfig::Tls(tls)) = &mut config.net.ssl {
273            if tls.cert.is_relative() {
274                tls.cert = dir.join(&tls.cert);
275            }
276            if tls.key.is_relative() {
277                tls.key = dir.join(&tls.key);
278            }
279
280            tls.cert = tls.cert.canonicalize()?;
281            tls.key = tls.key.canonicalize()?;
282        }
283
284        if let Some(db) = &config.storage.database.clone() {
285            config.storage.set_database_uri(db, config.directory())?;
286        }
287
288        Ok(config)
289    }
290
291    /// Set the server bind address.
292    pub fn set_bind_address(&mut self, addr: SocketAddr) {
293        self.net.bind = addr;
294    }
295
296    /// Server bind address.
297    pub fn bind_address(&self) -> &SocketAddr {
298        &self.net.bind
299    }
300
301    /// Parent directory of the configuration file.
302    fn directory(&self) -> PathBuf {
303        self.file
304            .as_ref()
305            .unwrap()
306            .parent()
307            .map(|p| p.to_path_buf())
308            .unwrap()
309    }
310
311    /// Get the backend implementation.
312    pub async fn backend(&self) -> Result<Backend> {
313        // Config file directory for relative file paths.
314        let dir = self.directory();
315
316        let path = &self.storage.path;
317        let path = if path.is_relative() {
318            dir.join(path)
319        } else {
320            path.to_owned()
321        };
322        let path = path.canonicalize()?;
323
324        let paths = Paths::new_server(&path);
325
326        let target = if let Some(uri) = &self.storage.database_uri {
327            tracing::debug!(
328                database_uri = % uri.as_uri_string(),
329                "server::db",
330            );
331            let mut client = open_file(uri.as_uri_string()).await?;
332            tracing::debug!("server::db::migrate",);
333            let report = migrate_client(&mut client).await?;
334            for migration in report.applied_migrations() {
335                tracing::debug!(
336                    name = %migration.name(),
337                    version = %migration.version(),
338                    "server::db::migration",);
339
340                println!(
341                    "Migration      {} {}",
342                    migration.name().green(),
343                    format!("v{}", migration.version()).green(),
344                );
345            }
346            BackendTarget::Database(paths.clone(), client)
347        } else {
348            BackendTarget::FileSystem(paths.clone())
349        };
350
351        let mut backend = Backend::new(paths, target);
352        backend.load_accounts().await?;
353        Ok(backend)
354    }
355}