diff --git a/ldk-server-cli/src/main.rs b/ldk-server-cli/src/main.rs index b0c346df..d6840d71 100644 --- a/ldk-server-cli/src/main.rs +++ b/ldk-server-cli/src/main.rs @@ -563,7 +563,20 @@ async fn main() { } let config_path = cli.config.map(PathBuf::from).or_else(get_default_config_path); - let config = config_path.as_ref().and_then(|p| load_config(p).ok()); + let config = match config_path.as_ref() { + None => None, + Some(path) => { + if path.is_file() { + let cfg = load_config(path).unwrap_or_else(|e| { + eprintln!("Failed to load config file '{}': {}", path.display(), e); + std::process::exit(1); + }); + Some(cfg) + } else { + None + } + }, + }; let api_key = resolve_api_key(cli.api_key, config.as_ref()).unwrap_or_else(|| { eprintln!("API key not provided. Use --api-key or ensure the api_key file exists at {DEFAULT_DIR}/[network]/api_key"); diff --git a/ldk-server-client/src/config.rs b/ldk-server-client/src/config.rs index f6f023ce..e2dca342 100644 --- a/ldk-server-client/src/config.rs +++ b/ldk-server-client/src/config.rs @@ -70,6 +70,7 @@ pub fn cert_path_for_storage_dir(storage_dir: &str) -> PathBuf { /// Top-level structure of the `ldk-server` configuration TOML file. #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Config { /// Node-level configuration. pub node: NodeConfig, @@ -81,6 +82,7 @@ pub struct Config { /// `[tls]` section of the configuration file. #[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] pub struct TlsConfig { /// Path to the server's TLS certificate in PEM format. pub cert_path: Option, @@ -88,6 +90,7 @@ pub struct TlsConfig { /// `[node]` section of the configuration file. #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct NodeConfig { /// Address of the `ldk-server` gRPC service. #[serde(default = "default_grpc_service_address")] @@ -97,6 +100,7 @@ pub struct NodeConfig { /// `[storage]` section of the configuration file. #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct StorageConfig { /// On-disk storage configuration. pub disk: Option, @@ -104,6 +108,7 @@ pub struct StorageConfig { /// `[storage.disk]` section of the configuration file. #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct DiskConfig { /// Directory used by the server to store its persistent data. pub dir_path: Option, @@ -202,6 +207,31 @@ mod tests { assert_eq!(config.node.grpc_service_address, DEFAULT_GRPC_SERVICE_ADDRESS); } + #[test] + fn config_rejects_unknown_fields() { + let top_level_err = toml::from_str::( + r#" + [node] + network = "regtest" + + [unknown] + option = true + "#, + ) + .unwrap_err(); + assert!(top_level_err.to_string().contains("unknown field `unknown`")); + + let node_err = toml::from_str::( + r#" + [node] + network = "regtest" + unknown = true + "#, + ) + .unwrap_err(); + assert!(node_err.to_string().contains("unknown field `unknown`")); + } + #[test] fn resolve_base_url_uses_cli_arg_first() { let config: Config = toml::from_str( diff --git a/ldk-server/src/util/config.rs b/ldk-server/src/util/config.rs index 61af98f5..318be1fa 100644 --- a/ldk-server/src/util/config.rs +++ b/ldk-server/src/util/config.rs @@ -460,6 +460,7 @@ impl ConfigBuilder { /// Configuration loaded from a TOML file. #[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] pub struct TomlConfig { node: Option, storage: Option, @@ -475,6 +476,7 @@ pub struct TomlConfig { } #[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] struct NodeConfig { network: Option, listening_addresses: Option>, @@ -487,16 +489,19 @@ struct NodeConfig { } #[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] struct StorageConfig { disk: Option, } #[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] struct DiskConfig { dir_path: Option, } #[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] struct BitcoindConfig { rpc_address: Option, rpc_user: Option, @@ -504,22 +509,26 @@ struct BitcoindConfig { } #[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] struct ElectrumConfig { server_url: String, } #[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] struct EsploraConfig { server_url: String, } #[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] struct LogConfig { level: Option, file: Option, } #[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] struct TomlTlsConfig { cert_path: Option, key_path: Option, @@ -527,6 +536,7 @@ struct TomlTlsConfig { } #[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] struct MetricsTomlConfig { enabled: Option, poll_metrics_interval: Option, @@ -535,11 +545,13 @@ struct MetricsTomlConfig { } #[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] struct TomlTorConfig { proxy_address: String, } #[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] struct HrnTomlConfig { mode: Option, dns_server_address: Option, @@ -645,12 +657,14 @@ fn parse_async_payments_role(role: &str) -> io::Result { } #[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] struct LiquidityConfig { lsps2_client: Option, lsps2_service: Option, } #[derive(Deserialize, Serialize, Debug)] +#[serde(deny_unknown_fields)] struct LSPSClientTomlConfig { node_pubkey: String, address: String, @@ -658,6 +672,7 @@ struct LSPSClientTomlConfig { } #[derive(Deserialize, Serialize, Debug)] +#[serde(deny_unknown_fields)] struct LSPS2ServiceTomlConfig { advertise_service: bool, channel_opening_fee_ppm: u32, @@ -1343,6 +1358,73 @@ mod tests { validate_missing!("network =", missing_field_msg("network")); } + #[test] + fn test_config_unknown_fields_in_file() { + let storage_path = std::env::temp_dir(); + let config_file_name = "test_config_unknown_fields_in_file.toml"; + + let mut args_config = empty_args_config(); + args_config.config_file = + Some(storage_path.join(config_file_name).to_string_lossy().to_string()); + + fs::write( + storage_path.join(config_file_name), + format!("{}\n[unknown]\noption = true\n", DEFAULT_CONFIG), + ) + .unwrap(); + let err = load_config(&args_config).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidData); + assert!(err.to_string().contains("unknown field `unknown`")); + + fs::write( + storage_path.join(config_file_name), + DEFAULT_CONFIG + .replace("network = \"regtest\"", "network = \"regtest\"\nunknown = true"), + ) + .unwrap(); + let err = load_config(&args_config).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidData); + assert!(err.to_string().contains("unknown field `unknown`")); + } + + #[test] + #[cfg(not(feature = "experimental-lsps2-support"))] + fn test_config_allows_unused_lsps2_service_config_without_feature() { + let storage_path = std::env::temp_dir(); + let config_file_name = + "test_config_allows_unused_lsps2_service_config_without_feature.toml"; + + let mut args_config = empty_args_config(); + args_config.config_file = + Some(storage_path.join(config_file_name).to_string_lossy().to_string()); + + let toml_config = r#" + [node] + network = "regtest" + + [bitcoind] + rpc_address = "127.0.0.1:8332" + rpc_user = "bitcoind-testuser" + rpc_password = "bitcoind-testpassword" + + [liquidity.lsps2_service] + advertise_service = false + channel_opening_fee_ppm = 1000 + channel_over_provisioning_ppm = 500000 + min_channel_opening_fee_msat = 10000000 + min_channel_lifetime = 4320 + max_client_to_self_delay = 1440 + min_payment_size_msat = 10000000 + max_payment_size_msat = 25000000000 + client_trusts_lsp = true + disable_client_reserve = false + "#; + + fs::write(storage_path.join(config_file_name), toml_config).unwrap(); + let config = load_config(&args_config).unwrap(); + assert!(config.lsps2_service_config.is_none()); + } + fn remove_config_line(config: &str, key: &str) -> String { config .lines() @@ -1573,7 +1655,6 @@ mod tests { let toml_config = r#" [node] network = "regtest" - rest_service_address = "127.0.0.1:3002" [bitcoind] rpc_address = "127.0.0.1:8332" @@ -1585,10 +1666,6 @@ mod tests { username = "admin" password = "password123" - [rabbitmq] - connection_string = "rabbitmq_connection_string" - exchange_name = "rabbitmq_exchange_name" - [liquidity.lsps2_service] advertise_service = false channel_opening_fee_ppm = 1000 # 0.1% fee @@ -1621,7 +1698,6 @@ mod tests { let toml_config = r#" [node] network = "regtest" - rest_service_address = "127.0.0.1:3002" [bitcoind] rpc_address = "127.0.0.1:8332" @@ -1632,10 +1708,6 @@ mod tests { enabled = true username = "admin" - [rabbitmq] - connection_string = "rabbitmq_connection_string" - exchange_name = "rabbitmq_exchange_name" - [liquidity.lsps2_service] advertise_service = false channel_opening_fee_ppm = 1000 # 0.1% fee