1#[cfg(any(test, feature = "testing"))]
8use super::storage_config::StorageConfigToml;
9use super::{
10 domain_port::DomainPort,
11 quota_config::{BandwidthQuota, PathLimit},
12 storage_config::StorageToml,
13 Domain, SignupMode,
14};
15
16use crate::{
17 data_directory::log_level::{LogLevel, TargetLevel},
18 persistence::sql::ConnectionString,
19 shared::toml_merge,
20};
21use serde::{Deserialize, Serialize};
22use std::{
23 fmt::Debug,
24 fs,
25 net::{IpAddr, SocketAddr},
26 num::{NonZeroU32, NonZeroU64},
27 path::Path,
28 str::FromStr,
29};
30use url::Url;
31
32pub const DEFAULT_CONFIG: &str = include_str!("config.default.toml");
34
35pub const SAMPLE_CONFIG: &str = include_str!("../../config.sample.toml");
37
38#[derive(Debug, thiserror::Error)]
40pub enum ConfigReadError {
41 #[error("config file not found: {0}")]
43 ConfigFileNotFound(#[from] std::io::Error),
44 #[error("config file is not valid TOML: {0}")]
46 ConfigFileNotValid(#[from] toml::de::Error),
47 #[error("failed to merge embedded and user TOML: {0}")]
49 ConfigMergeError(String),
50}
51
52#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
55pub struct PkdnsToml {
56 pub public_ip: IpAddr,
57 pub public_pubky_tls_port: Option<u16>,
58 pub public_icann_http_port: Option<u16>,
59 pub icann_domain: Option<Domain>,
60 pub user_keys_republisher_interval: u64,
61 pub dht_bootstrap_nodes: Option<Vec<DomainPort>>,
62 pub dht_relay_nodes: Option<Vec<Url>>,
63 pub dht_request_timeout_ms: Option<NonZeroU64>,
64}
65
66#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
67pub struct DriveToml {
68 pub pubky_listen_socket: SocketAddr,
69 pub icann_listen_socket: SocketAddr,
70 pub rate_limits: Vec<PathLimit>,
72}
73
74#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
86pub struct DefaultQuotasToml {
87 pub rate_read: Option<BandwidthQuota>,
90 pub rate_write: Option<BandwidthQuota>,
93 pub rate_read_burst: Option<NonZeroU32>,
96 pub rate_write_burst: Option<NonZeroU32>,
99 pub unauthenticated_ip_rate_read: Option<BandwidthQuota>,
102}
103
104#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
106pub struct AdminToml {
107 pub enabled: bool,
109 pub listen_socket: SocketAddr,
111 pub admin_password: String,
113}
114
115#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
116pub struct GeneralToml {
117 pub signup_mode: SignupMode,
118 #[deprecated(
122 since = "0.7.0",
123 note = "use `storage.default_quota_mb` instead; this field is only resolved at TOML parse time"
124 )]
125 #[serde(default)]
126 pub user_storage_quota_mb: u64,
127 pub database_url: ConnectionString,
128}
129
130#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
132pub struct LoggingToml {
133 pub level: LogLevel,
135 pub module_levels: Vec<TargetLevel>,
137}
138
139#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
141pub struct MetricsToml {
142 pub enabled: bool,
144 pub listen_socket: SocketAddr,
146}
147
148#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
150pub struct ConfigToml {
151 pub general: GeneralToml,
153 pub drive: DriveToml,
155 pub storage: StorageToml,
157 pub default_quotas: DefaultQuotasToml,
159 pub admin: AdminToml,
161 pub metrics: MetricsToml,
163 pub pkdns: PkdnsToml,
165 pub logging: Option<LoggingToml>,
168}
169
170impl Default for ConfigToml {
171 fn default() -> Self {
172 ConfigToml::from_str(DEFAULT_CONFIG).expect("Embedded config.default.toml must be valid")
173 }
174}
175
176impl Default for DriveToml {
177 fn default() -> Self {
178 ConfigToml::default().drive
179 }
180}
181
182impl Default for AdminToml {
183 fn default() -> Self {
184 ConfigToml::default().admin
185 }
186}
187
188impl Default for PkdnsToml {
189 fn default() -> Self {
190 ConfigToml::default().pkdns
191 }
192}
193
194impl ConfigToml {
195 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, ConfigReadError> {
203 let raw = fs::read_to_string(path)?;
204 Self::from_str_with_defaults(&raw)
205 }
206
207 pub fn from_str_with_defaults(raw: &str) -> Result<Self, ConfigReadError> {
209 let default_val: toml::Value = DEFAULT_CONFIG
211 .parse()
212 .expect("embedded defaults invalid TOML");
213 let user_val: toml::Value = raw.parse()?;
215 let merged_val = toml_merge::merge_with_options(default_val, user_val, true)
217 .map_err(|e| ConfigReadError::ConfigMergeError(e.to_string()))?;
218
219 let mut config: Self = merged_val.try_into()?;
221 config.resolve_legacy_quotas();
222 Ok(config)
223 }
224
225 pub fn sample_string() -> String {
228 SAMPLE_CONFIG
229 .lines()
230 .map(|line| {
231 let trimmed = line.trim_start();
232 let is_comment = trimmed.starts_with('#');
233 if !is_comment && !trimmed.is_empty() {
234 format!("# {}", line)
235 } else {
236 line.to_string()
237 }
238 })
239 .collect::<Vec<String>>()
240 .join("\n")
241 }
242
243 #[cfg(any(test, feature = "testing"))]
247 pub fn default_test_config() -> Self {
248 let mut config = Self::default();
249 config.general.database_url = ConnectionString::default_test_db(); config.general.signup_mode = SignupMode::Open;
251 config.drive.icann_listen_socket = SocketAddr::from(([127, 0, 0, 1], 0));
253 config.drive.pubky_listen_socket = SocketAddr::from(([127, 0, 0, 1], 0));
254 config.admin.enabled = true; config.admin.listen_socket = SocketAddr::from(([127, 0, 0, 1], 0));
256 config.pkdns.icann_domain =
257 Some(Domain::from_str("localhost").expect("localhost is a valid domain"));
258 config.pkdns.dht_relay_nodes = None;
259 config.storage.backend = StorageConfigToml::InMemory;
260 config.logging = None;
261 config
262 }
263
264 #[cfg(any(test, feature = "testing"))]
267 pub fn minimal_test_config() -> Self {
268 let mut config = Self::default_test_config();
269 config.admin.enabled = false;
270 config.metrics.enabled = false;
271 config
272 }
273
274 #[cfg(any(test, feature = "testing"))]
280 #[deprecated(
281 since = "0.5.0",
282 note = "Use default_test_config() or minimal_test_config() directly for explicit behavior"
283 )]
284 pub fn test() -> Self {
285 Self::default_test_config()
286 }
287
288 fn resolve_legacy_quotas(&mut self) {
292 if self.storage.default_quota_mb.is_none() {
293 #[allow(deprecated)]
294 let legacy = self.general.user_storage_quota_mb;
295 self.storage.default_quota_mb = match legacy {
296 0 => None,
297 n => Some(n),
298 };
299 }
300 }
301}
302
303impl FromStr for ConfigToml {
304 type Err = toml::de::Error;
305
306 fn from_str(s: &str) -> Result<Self, Self::Err> {
307 let mut config: Self = toml::from_str(s)?;
308 config.resolve_legacy_quotas();
309 Ok(config)
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use crate::data_directory::log_level::LogLevel;
316
317 use super::*;
318 use std::{
319 net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
320 str::FromStr,
321 };
322
323 #[test]
324 fn test_default_config() {
325 let c = ConfigToml::default();
326 assert_eq!(c.general.signup_mode, SignupMode::TokenRequired);
327 #[allow(deprecated)]
328 {
329 assert_eq!(c.general.user_storage_quota_mb, 0);
330 }
331 assert_eq!(
332 c.drive.icann_listen_socket,
333 SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 6286))
334 );
335 assert_eq!(
336 c.pkdns.icann_domain,
337 Some(Domain::from_str("localhost").unwrap())
338 );
339 assert_eq!(
340 c.drive.pubky_listen_socket,
341 SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 6287))
342 );
343 assert_eq!(
344 c.admin.listen_socket,
345 SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 6288))
346 );
347 assert_eq!(c.admin.admin_password, "admin");
348 assert_eq!(c.pkdns.public_ip, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
349 assert_eq!(c.pkdns.public_pubky_tls_port, None);
350 assert_eq!(c.pkdns.public_icann_http_port, None);
351 assert_eq!(c.pkdns.user_keys_republisher_interval, 14400);
352 assert_eq!(c.pkdns.dht_bootstrap_nodes, None);
353 assert_eq!(c.pkdns.dht_request_timeout_ms, None);
354 assert_eq!(c.drive.rate_limits.len(), 1);
355 assert_eq!(c.drive.rate_limits[0].path.0, "/signup_tokens/*");
356 assert_eq!(c.default_quotas, DefaultQuotasToml::default());
357 assert_eq!(c.storage.default_quota_mb, None);
358 assert_eq!(c.storage.backend, StorageConfigToml::FileSystem);
359 assert_eq!(
360 c.logging,
361 Some(LoggingToml {
362 level: LogLevel::from_str("info").unwrap(),
363 module_levels: vec![
364 TargetLevel::from_str("pubky_homeserver=debug").unwrap(),
365 TargetLevel::from_str("tower_http=debug").unwrap()
366 ],
367 })
368 );
369 }
370
371 #[test]
372 fn test_sample_config() {
373 ConfigToml::from_str(SAMPLE_CONFIG).expect("Embedded config.sample.toml must be valid");
375 }
376
377 #[test]
378 fn test_sample_config_commented_out() {
379 let s = ConfigToml::sample_string();
382 let parsed: ConfigToml =
383 ConfigToml::from_str_with_defaults(&s).expect("Should be valid config file");
384 assert_eq!(parsed, ConfigToml::default());
385 }
386
387 #[test]
388 fn test_empty_config() {
389 let s = "[general]\nsignup_mode = \"open\"\n";
391 let parsed: ConfigToml = ConfigToml::from_str_with_defaults(s).unwrap();
392 assert_eq!(parsed.general.signup_mode, SignupMode::Open);
394 assert_eq!(parsed.admin, ConfigToml::default().admin);
396 assert_eq!(parsed.logging, ConfigToml::default().logging);
397 }
398
399 #[test]
400 fn test_merged_config() {
401 let s = "[logging]\nlevel=\"trace\"\nmodule_levels = [ ]";
403 let merged: ConfigToml = ConfigToml::from_str_with_defaults(s).unwrap();
404 assert_eq!(merged.drive.rate_limits.len(), 1);
406 assert_eq!(merged.drive.rate_limits[0].path.0, "/signup_tokens/*");
407 let expected_logging = Some(LoggingToml {
408 level: LogLevel::from_str("trace").unwrap(),
409 module_levels: vec![],
410 });
411 assert_eq!(merged.logging, expected_logging);
412 }
413
414 #[test]
415 fn test_legacy_general_storage_quota_migrated() {
416 let s = "[general]\nuser_storage_quota_mb = 500\n";
418 let parsed = ConfigToml::from_str_with_defaults(s).unwrap();
419 assert_eq!(parsed.storage.default_quota_mb, Some(500));
420 }
421}