254 lines
10 KiB
Rust
254 lines
10 KiB
Rust
/*
|
|
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
|
|
* Source-Available Code / Dual-Licensed.
|
|
*
|
|
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
|
|
* Commercial or production use requires a valid paid license.
|
|
* See LICENSE file in repository root for details.
|
|
*/
|
|
|
|
mod smtp;
|
|
mod values;
|
|
|
|
use smtp::load_smtp;
|
|
use std::{net::IpAddr, path::Path};
|
|
use values::ConfigValues;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum AuthorizationType {
|
|
Local,
|
|
Ldap,
|
|
Ad,
|
|
}
|
|
|
|
impl AuthorizationType {
|
|
fn from_values(values: &ConfigValues) -> Result<Self, Box<dyn std::error::Error>> {
|
|
match values
|
|
.get("AUTHORIZATION_TYPE", "local")
|
|
.trim()
|
|
.to_ascii_lowercase()
|
|
.as_str()
|
|
{
|
|
"local" => Ok(Self::Local),
|
|
"ldap" => Ok(Self::Ldap),
|
|
"ad" => Ok(Self::Ad),
|
|
_ => Err("AUTHORIZATION_TYPE must be one of: local, ldap, ad".into()),
|
|
}
|
|
}
|
|
|
|
pub fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::Local => "local",
|
|
Self::Ldap => "ldap",
|
|
Self::Ad => "ad",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Config {
|
|
pub host: IpAddr,
|
|
pub port: u16,
|
|
pub database_url: String,
|
|
pub database_max_connections: u32,
|
|
pub static_dir: String,
|
|
pub files_dir: String,
|
|
pub storage: crate::storage::StorageConfig,
|
|
pub upload_max_size_bytes: usize,
|
|
pub guest_upload_enabled: bool,
|
|
pub guest_upload_max_size_bytes: usize,
|
|
pub asset_version: String,
|
|
pub asset_cache_max_age_seconds: u64,
|
|
pub http_compression: bool,
|
|
pub file_cache_max_age_seconds: u64,
|
|
pub files_public_url: Option<String>,
|
|
pub smtp: Option<crate::state::SmtpConfig>,
|
|
pub registration_enabled: bool,
|
|
pub account_confirmation_required: bool,
|
|
pub share_confirmation_required: bool,
|
|
pub frontend_log_level: String,
|
|
pub anonymous_access_token_ttl_days: i64,
|
|
pub user_session_ttl_days: i64,
|
|
pub unconfirmed_account_ttl_days: i64,
|
|
pub authorization_type: AuthorizationType,
|
|
pub ldap: Option<crate::auth::ldap::LdapConfig>,
|
|
}
|
|
|
|
impl Config {
|
|
pub fn load(path: Option<&Path>) -> Result<Self, Box<dyn std::error::Error>> {
|
|
let values = ConfigValues::load(path)?;
|
|
let host = values.get("APP_HOST", "127.0.0.1").parse()?;
|
|
let port = values.get("APP_PORT", "3000").parse()?;
|
|
let database_max_connections = values.get("DATABASE_MAX_CONNECTIONS", "8").parse()?;
|
|
let upload_max_size_mb = values.positive_u64("UPLOAD_MAX_SIZE_MB", 20)?;
|
|
let guest_upload_enabled = values.bool("GUEST_UPLOAD_ENABLED", false)?;
|
|
let guest_upload_max_size_mb = values.positive_u64("GUEST_UPLOAD_MAX_SIZE_MB", 5)?;
|
|
let anonymous_access_token_ttl_days =
|
|
values.positive_i64("ANONYMOUS_ACCESS_TOKEN_TTL_DAYS", 7)?;
|
|
let user_session_ttl_days = values.positive_i64("USER_SESSION_TTL_DAYS", 3)?;
|
|
let unconfirmed_account_ttl_days =
|
|
values.positive_i64("UNCONFIRMED_ACCOUNT_TTL_DAYS", 3)?;
|
|
let files_dir = values.get("FILES_DIR", "data/files");
|
|
let files_public_url =
|
|
crate::file_urls::normalize_public_base(values.optional("FILES_PUBLIC_URL"))?;
|
|
|
|
let storage = match values
|
|
.get("STORAGE_DRIVER", "local")
|
|
.trim()
|
|
.to_ascii_lowercase()
|
|
.as_str()
|
|
{
|
|
"local" => crate::storage::StorageConfig::Local {
|
|
root: files_dir.clone().into(),
|
|
},
|
|
"s3" => crate::storage::StorageConfig::S3 {
|
|
endpoint: values.optional("S3_ENDPOINT"),
|
|
region: values.get("S3_REGION", "us-east-1"),
|
|
bucket: values.required("S3_BUCKET", "STORAGE_DRIVER=s3")?,
|
|
access_key: values.required("S3_ACCESS_KEY", "STORAGE_DRIVER=s3")?,
|
|
secret_key: values.required("S3_SECRET_KEY", "STORAGE_DRIVER=s3")?,
|
|
force_path_style: values.bool("S3_FORCE_PATH_STYLE", false)?,
|
|
},
|
|
_ => return Err("STORAGE_DRIVER must be local or s3".into()),
|
|
};
|
|
|
|
let authorization_type = AuthorizationType::from_values(&values)?;
|
|
let ldap = match authorization_type {
|
|
AuthorizationType::Local => None,
|
|
AuthorizationType::Ldap | AuthorizationType::Ad => {
|
|
let context = format!("AUTHORIZATION_TYPE={}", authorization_type.as_str());
|
|
let (default_filter, default_username_attribute) = match authorization_type {
|
|
AuthorizationType::Ldap => ("(uid={username})", "uid"),
|
|
AuthorizationType::Ad => (
|
|
"(|(sAMAccountName={username})(userPrincipalName={username}))",
|
|
"sAMAccountName",
|
|
),
|
|
AuthorizationType::Local => unreachable!(),
|
|
};
|
|
Some(crate::auth::ldap::LdapConfig {
|
|
url: values.required("LDAP_URL", &context)?,
|
|
starttls: values.bool("LDAP_STARTTLS", false)?,
|
|
bind_dn: values.get("LDAP_BIND_DN", ""),
|
|
bind_password: values.get("LDAP_BIND_PASSWORD", ""),
|
|
base_dn: values.required("LDAP_BASE_DN", &context)?,
|
|
user_filter: values.get("LDAP_USER_FILTER", default_filter),
|
|
username_attribute: values
|
|
.get("LDAP_USERNAME_ATTRIBUTE", default_username_attribute),
|
|
email_attribute: values.get("LDAP_EMAIL_ATTRIBUTE", "mail"),
|
|
display_name_attribute: values
|
|
.get("LDAP_DISPLAY_NAME_ATTRIBUTE", "displayName"),
|
|
external_id_attribute: values.get(
|
|
"LDAP_EXTERNAL_ID_ATTRIBUTE",
|
|
match authorization_type {
|
|
AuthorizationType::Ldap => "entryUUID",
|
|
AuthorizationType::Ad => "objectGUID",
|
|
AuthorizationType::Local => unreachable!(),
|
|
},
|
|
),
|
|
organization: values.get("LDAP_ORGANIZATION", "organization"),
|
|
provider: authorization_type.as_str().to_owned(),
|
|
email_required: values.bool("LDAP_EMAIL_REQUIRED", true)?,
|
|
link_existing_by_email: values.bool("LDAP_LINK_EXISTING_BY_EMAIL", false)?,
|
|
tls_verify: values.bool("LDAP_TLS_VERIFY", true)?,
|
|
connect_timeout_seconds: values
|
|
.positive_u64("LDAP_CONNECT_TIMEOUT_SECONDS", 5)?,
|
|
operation_timeout_seconds: values
|
|
.positive_u64("LDAP_OPERATION_TIMEOUT_SECONDS", 10)?,
|
|
})
|
|
}
|
|
};
|
|
|
|
let smtp = load_smtp(&values)?;
|
|
|
|
let config = Self {
|
|
host,
|
|
port,
|
|
database_url: values.get("DATABASE_URL", "sqlite:///data/db/rustpad.db?mode=rwc"),
|
|
database_max_connections,
|
|
static_dir: values.get("STATIC_DIR", "static"),
|
|
files_dir,
|
|
storage,
|
|
upload_max_size_bytes: megabytes_to_bytes("UPLOAD_MAX_SIZE_MB", upload_max_size_mb)?,
|
|
guest_upload_enabled,
|
|
guest_upload_max_size_bytes: megabytes_to_bytes(
|
|
"GUEST_UPLOAD_MAX_SIZE_MB",
|
|
guest_upload_max_size_mb,
|
|
)?,
|
|
asset_version: format!("{}-ui3", env!("CARGO_PKG_VERSION")),
|
|
http_compression: values.bool("HTTP_COMPRESSION", false)?,
|
|
asset_cache_max_age_seconds: values
|
|
.nonnegative_u64("ASSET_CACHE_MAX_AGE_SECONDS", 600)?,
|
|
file_cache_max_age_seconds: values
|
|
.nonnegative_u64("FILE_CACHE_MAX_AGE_SECONDS", 600)?,
|
|
files_public_url,
|
|
smtp,
|
|
registration_enabled: values.bool("REGISTRATION_ENABLED", false)?,
|
|
account_confirmation_required: values.bool("ACCOUNT_CONFIRMATION_REQUIRED", false)?,
|
|
share_confirmation_required: values.bool("SHARE_CONFIRMATION_REQUIRED", false)?,
|
|
frontend_log_level: values.log_level("FRONTEND_LOG_LEVEL", "warn")?,
|
|
anonymous_access_token_ttl_days,
|
|
user_session_ttl_days,
|
|
unconfirmed_account_ttl_days,
|
|
authorization_type,
|
|
ldap,
|
|
};
|
|
config.validate()?;
|
|
Ok(config)
|
|
}
|
|
|
|
fn validate(&self) -> Result<(), Box<dyn std::error::Error>> {
|
|
if !(self.database_url.starts_with("sqlite:")
|
|
|| self.database_url.starts_with("postgres:")
|
|
|| self.database_url.starts_with("postgresql:")
|
|
|| self.database_url.starts_with("mysql:"))
|
|
{
|
|
return Err("DATABASE_URL must use sqlite, postgres/postgresql, or mysql".into());
|
|
}
|
|
if self.database_max_connections == 0 {
|
|
return Err("DATABASE_MAX_CONNECTIONS must be greater than 0".into());
|
|
}
|
|
if self.static_dir.trim().is_empty() || self.files_dir.trim().is_empty() {
|
|
return Err("STATIC_DIR and FILES_DIR cannot be empty".into());
|
|
}
|
|
if let Some(smtp) = &self.smtp {
|
|
if !(smtp.public_url.starts_with("http://") || smtp.public_url.starts_with("https://"))
|
|
{
|
|
return Err("PUBLIC_URL must start with http:// or https://".into());
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn upload_body_limit_bytes(&self) -> usize {
|
|
multipart_body_limit_bytes(
|
|
self.upload_max_size_bytes,
|
|
self.guest_upload_enabled,
|
|
self.guest_upload_max_size_bytes,
|
|
)
|
|
}
|
|
}
|
|
|
|
fn multipart_body_limit_bytes(
|
|
user_limit_bytes: usize,
|
|
guest_upload_enabled: bool,
|
|
guest_limit_bytes: usize,
|
|
) -> usize {
|
|
let file_limit = if guest_upload_enabled {
|
|
user_limit_bytes.max(guest_limit_bytes)
|
|
} else {
|
|
user_limit_bytes
|
|
};
|
|
file_limit.saturating_add(1024 * 1024)
|
|
}
|
|
|
|
fn megabytes_to_bytes(name: &str, megabytes: u64) -> Result<usize, Box<dyn std::error::Error>> {
|
|
let bytes = megabytes
|
|
.checked_mul(1024 * 1024)
|
|
.ok_or_else(|| format!("{name} is too large"))?;
|
|
usize::try_from(bytes).map_err(|_| format!("{name} is too large").into())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "../tests/config.rs"]
|
|
mod tests;
|