use super::backend::Backend;
use super::{Error, Result};
use colored::Colorize;
use serde::{Deserialize, Serialize};
use sos_backend::BackendTarget;
use sos_core::{AccountId, Paths};
use sos_database::{migrations::migrate_client, open_file};
use sos_vfs as vfs;
use std::{
collections::HashSet,
net::{IpAddr, Ipv4Addr, SocketAddr},
path::{Path, PathBuf},
};
use url::Url;
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct ServerConfig {
pub storage: StorageConfig,
pub log: LogConfig,
pub access: Option<AccessControlConfig>,
pub net: NetworkConfig,
#[serde(skip)]
file: Option<PathBuf>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct AccessControlConfig {
pub allow: Option<HashSet<AccountId>>,
pub deny: Option<HashSet<AccountId>>,
}
impl AccessControlConfig {
pub fn is_allowed_access(&self, account_id: &AccountId) -> bool {
let has_definitions = self.allow.is_some() || self.deny.is_some();
if has_definitions {
match (&self.deny, &self.allow) {
(Some(deny), None) => {
if deny.iter().any(|a| a == account_id) {
return false;
}
true
}
(None, Some(allow)) => {
if allow.iter().any(|a| a == account_id) {
return true;
}
false
}
(Some(deny), Some(allow)) => {
if allow.iter().any(|a| a == account_id) {
return true;
}
if deny.iter().any(|a| a == account_id) {
return false;
}
false
}
_ => true,
}
} else {
true
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogConfig {
pub directory: PathBuf,
pub name: String,
pub level: String,
}
impl Default for LogConfig {
fn default() -> Self {
Self {
directory: PathBuf::from("logs"),
name: "sos-server.log".to_string(),
level: "sos_server=info".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct NetworkConfig {
pub bind: SocketAddr,
pub ssl: Option<SslConfig>,
pub cors: Option<CorsConfig>,
}
impl Default for NetworkConfig {
fn default() -> Self {
Self {
bind: SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
5053,
),
ssl: Default::default(),
cors: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase", untagged)]
pub enum SslConfig {
Tls(TlsConfig),
#[cfg(feature = "acme")]
Acme(AcmeConfig),
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct TlsConfig {
pub cert: PathBuf,
pub key: PathBuf,
}
#[cfg(feature = "acme")]
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct AcmeConfig {
pub cache: PathBuf,
pub domains: Vec<String>,
pub email: Vec<String>,
pub production: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CorsConfig {
pub origins: Vec<Url>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageConfig {
pub path: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
pub database: Option<String>,
#[serde(skip)]
pub database_uri: Option<UriOrPath>,
}
#[derive(Debug, Clone)]
pub enum UriOrPath {
Uri(http::Uri),
Path(PathBuf),
}
impl UriOrPath {
pub fn as_uri_string(&self) -> String {
match self {
UriOrPath::Uri(uri) => uri.to_string(),
UriOrPath::Path(path) => format!("file:{}", path.display()),
}
}
}
impl StorageConfig {
#[doc(hidden)]
fn set_database_uri(
&mut self,
db: &str,
base_dir: impl AsRef<Path>,
) -> Result<()> {
let uri = if db.starts_with("file:") {
UriOrPath::Uri(db.parse()?)
} else {
let path = PathBuf::from(db);
if path.is_relative() {
let path = base_dir.as_ref().join(path);
if !path.exists() {
std::fs::File::create(&path)?;
}
UriOrPath::Path(path.canonicalize()?)
} else {
UriOrPath::Path(path)
}
};
self.database_uri = Some(uri);
Ok(())
}
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
path: PathBuf::from("."),
database: None,
database_uri: None,
}
}
}
impl ServerConfig {
pub async fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
if !vfs::try_exists(path.as_ref()).await? {
return Err(Error::NotFile(path.as_ref().to_path_buf()));
}
let contents = vfs::read_to_string(path.as_ref()).await?;
let mut config: ServerConfig = toml::from_str(&contents)?;
config.file = Some(path.as_ref().canonicalize()?);
let dir = config.directory();
if config.log.directory.is_relative() {
config.log.directory = dir.join(&config.log.directory);
if !config.log.directory.exists() {
vfs::create_dir_all(&config.log.directory).await?;
}
config.log.directory = config.log.directory.canonicalize()?;
}
if let Some(SslConfig::Tls(tls)) = &mut config.net.ssl {
if tls.cert.is_relative() {
tls.cert = dir.join(&tls.cert);
}
if tls.key.is_relative() {
tls.key = dir.join(&tls.key);
}
tls.cert = tls.cert.canonicalize()?;
tls.key = tls.key.canonicalize()?;
}
if let Some(db) = &config.storage.database.clone() {
config.storage.set_database_uri(db, config.directory())?;
}
Ok(config)
}
pub fn set_bind_address(&mut self, addr: SocketAddr) {
self.net.bind = addr;
}
pub fn bind_address(&self) -> &SocketAddr {
&self.net.bind
}
fn directory(&self) -> PathBuf {
self.file
.as_ref()
.unwrap()
.parent()
.map(|p| p.to_path_buf())
.unwrap()
}
pub async fn backend(&self) -> Result<Backend> {
let dir = self.directory();
let path = &self.storage.path;
let path = if path.is_relative() {
dir.join(path)
} else {
path.to_owned()
};
let path = path.canonicalize()?;
let paths = Paths::new_server(&path);
let target = if let Some(uri) = &self.storage.database_uri {
tracing::debug!(
database_uri = % uri.as_uri_string(),
"server::db",
);
let mut client = open_file(uri.as_uri_string()).await?;
tracing::debug!("server::db::migrate",);
let report = migrate_client(&mut client).await?;
for migration in report.applied_migrations() {
tracing::debug!(
name = %migration.name(),
version = %migration.version(),
"server::db::migration",);
println!(
"Migration {} {}",
migration.name().green(),
format!("v{}", migration.version()).green(),
);
}
BackendTarget::Database(paths.clone(), client)
} else {
BackendTarget::FileSystem(paths.clone())
};
let mut backend = Backend::new(paths, target);
backend.load_accounts().await?;
Ok(backend)
}
}