Skip to main content

pubky_homeserver/data_directory/
config_toml.rs

1//! Configuration file for the homeserver.
2//!
3//! All default values live exclusively in `config.default.toml`.
4//! This module embeds that file at compile-time, parses it once,
5//! and lets callers optionally layer their own TOML on top.
6
7#[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
32/// Embedded copy of the default configuration (single source of truth for defaults)
33pub const DEFAULT_CONFIG: &str = include_str!("config.default.toml");
34
35/// Example configuration file
36pub const SAMPLE_CONFIG: &str = include_str!("../../config.sample.toml");
37
38/// Error that can occur when reading a configuration file.
39#[derive(Debug, thiserror::Error)]
40pub enum ConfigReadError {
41    /// The file did not exist or could not be read.
42    #[error("config file not found: {0}")]
43    ConfigFileNotFound(#[from] std::io::Error),
44    /// The TOML was syntactically invalid.
45    #[error("config file is not valid TOML: {0}")]
46    ConfigFileNotValid(#[from] toml::de::Error),
47    /// Failed to merge defaults with overrides.
48    #[error("failed to merge embedded and user TOML: {0}")]
49    ConfigMergeError(String),
50}
51
52/// Config structs
53
54#[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    /// Per-path request-count rate limits.
71    pub rate_limits: Vec<PathLimit>,
72}
73
74/// Default bandwidth limits for the rate limiter.
75///
76/// Per-user defaults (`rate_read`, `rate_write`) are the system-wide
77/// fallback values used when a user's quota is "Default" (NULL in DB).
78/// Per-user overrides are managed via the admin API:
79///   `PATCH /users/{pubkey}/quota`
80///
81/// `unauthenticated_ip_rate_read` is a fixed server-level limit for
82/// anonymous requests (not overridable per-user).
83///
84/// Consumed by `BandwidthQuotaLimitLayer`.
85#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
86pub struct DefaultQuotasToml {
87    /// Default bandwidth limit for user reads / downloads (e.g. "10mb/s").
88    /// Per-user DB overrides take precedence. `None` means no read throttling.
89    pub rate_read: Option<BandwidthQuota>,
90    /// Default bandwidth limit for user writes / uploads (e.g. "5mb/s").
91    /// Per-user DB overrides take precedence. `None` means no write throttling.
92    pub rate_write: Option<BandwidthQuota>,
93    /// Default burst for read rate, in the rate's natural unit (e.g. MB for "…mb/s").
94    /// Per-user DB overrides take precedence. `None` means burst equals rate.
95    pub rate_read_burst: Option<NonZeroU32>,
96    /// Default burst for write rate, in the rate's natural unit (e.g. MB for "…mb/s").
97    /// Per-user DB overrides take precedence. `None` means burst equals rate.
98    pub rate_write_burst: Option<NonZeroU32>,
99    /// Server-level bandwidth limit for unauthenticated IP reads (e.g. "1mb/s").
100    /// `None` means no read throttling for unauthenticated requests.
101    pub unauthenticated_ip_rate_read: Option<BandwidthQuota>,
102}
103
104/// Admin server configuration
105#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
106pub struct AdminToml {
107    /// Enable or disable the admin server
108    pub enabled: bool,
109    /// Socket address for the admin HTTP server
110    pub listen_socket: SocketAddr,
111    /// Password for admin authentication
112    pub admin_password: String,
113}
114
115#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
116pub struct GeneralToml {
117    pub signup_mode: SignupMode,
118    /// Deprecated: use `[storage].default_quota_mb` instead.
119    /// Kept for backwards compatibility: `0` means unlimited.
120    /// Ignored when `[storage].default_quota_mb` is set.
121    #[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/// A config for Homeserver tracing subscriber configuration
131#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
132pub struct LoggingToml {
133    /// Main log level for global tracing_subscriber
134    pub level: LogLevel,
135    /// Per-module target log filters for global tracing_subscriber
136    pub module_levels: Vec<TargetLevel>,
137}
138
139/// Metrics server configuration
140#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
141pub struct MetricsToml {
142    /// Enable or disable the metrics server
143    pub enabled: bool,
144    /// Socket address for Prometheus metrics endpoint. Should be isolated from public network.
145    pub listen_socket: SocketAddr,
146}
147
148/// The overall application configuration, composed of several subsections.
149#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
150pub struct ConfigToml {
151    /// General application settings (signup mode, quotas, backups).
152    pub general: GeneralToml,
153    /// File‐drive API settings (listen sockets for Pubky TLS and HTTP).
154    pub drive: DriveToml,
155    /// Storage configuration: backend selection and default storage quota.
156    pub storage: StorageToml,
157    /// Default bandwidth limits for the rate limiter. Overridable per-user via admin API.
158    pub default_quotas: DefaultQuotasToml,
159    /// Administrative API configuration.
160    pub admin: AdminToml,
161    /// Metrics server configuration.
162    pub metrics: MetricsToml,
163    /// Peer‐to‐peer DHT / PKDNS settings (public endpoints, bootstrap, relays).
164    pub pkdns: PkdnsToml,
165    /// Logging configuration. If provided, the homeserver instance attempts to init
166    /// global tracing:Subscriber. If environment variables are set, they override config settings
167    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    /// Read and parse a configuration file, overlaying it on top of the embedded defaults.
196    ///
197    /// # Arguments
198    /// * `path` - The path to the TOML configuration file
199    ///
200    /// # Returns
201    /// * `Result<ConfigToml>` - The parsed configuration or an error if reading/parsing fails
202    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    /// Parse a raw TOML string, overlaying it on top of the embedded defaults.
208    pub fn from_str_with_defaults(raw: &str) -> Result<Self, ConfigReadError> {
209        // 1. Parse the embedded defaults
210        let default_val: toml::Value = DEFAULT_CONFIG
211            .parse()
212            .expect("embedded defaults invalid TOML");
213        // 2. Parse the user's overrides
214        let user_val: toml::Value = raw.parse()?;
215        // 3. Deep‐merge
216        let merged_val = toml_merge::merge_with_options(default_val, user_val, true)
217            .map_err(|e| ConfigReadError::ConfigMergeError(e.to_string()))?;
218
219        // 4. Deserialize into our strongly typed struct (can fail with toml::de::Error)
220        let mut config: Self = merged_val.try_into()?;
221        config.resolve_legacy_quotas();
222        Ok(config)
223    }
224
225    /// Render the embedded sample config but comment out every value,
226    /// producing a handy template for end-users.
227    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    /// Returns a default config tuned for unit tests with full features enabled.
244    /// Note: Admin server is enabled, Metrics server is disabled.
245    /// Use this for backward compatibility with external codebases using `EphemeralTestnet::start()`.
246    #[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(); // Mark this db as test. This indicates that the db is not real.
250        config.general.signup_mode = SignupMode::Open;
251        // Use ephemeral ports (0) so parallel tests don't collide.
252        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; // Enabled for backward compat
255        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    /// Returns a minimal test config with admin/metrics disabled.
265    /// Use for fast internal tests that don't need extra servers.
266    #[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    /// Returns the default test configuration for backward compatibility.
275    ///
276    /// # Deprecated
277    /// Use [`Self::default_test_config()`] for full-featured config (admin enabled),
278    /// or [`Self::minimal_test_config()`] for lightweight tests (admin/metrics disabled).
279    #[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    /// Migrate deprecated `[general].user_storage_quota_mb` into
289    /// `[storage].default_quota_mb`. Called once at parse time so
290    /// downstream code only checks one place.
291    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        // Validate that the sample config can be parsed
374        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        // Sanity check that the sample config is valid even when the variables are commented out.
380        // An empty or fully commented out .toml should still be equal to the default ConfigToml
381        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        // Test that a minimal config with only the general section works
390        let s = "[general]\nsignup_mode = \"open\"\n";
391        let parsed: ConfigToml = ConfigToml::from_str_with_defaults(s).unwrap();
392        // Check that explicitly set values are preserved
393        assert_eq!(parsed.general.signup_mode, SignupMode::Open);
394        // Other fields that were not set (left empty) should still match the default.
395        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        // Test that a minimal config with optional logging section with empty module_levels
402        let s = "[logging]\nlevel=\"trace\"\nmodule_levels = [ ]";
403        let merged: ConfigToml = ConfigToml::from_str_with_defaults(s).unwrap();
404        // Default rate limits should be preserved from defaults
405        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        // general.user_storage_quota_mb should migrate to storage.default_quota_mb
417        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}