From 639655c72a5d3c05f52ad6dce433cea97665423c Mon Sep 17 00:00:00 2001 From: benthecarman Date: Wed, 14 Jan 2026 14:46:37 -0600 Subject: [PATCH] Create default data directory at ~/.ldk-server Previously, the daemon required a config file path argument and the CLI required explicit --api-key and --tls-cert flags. Now both default to reading from ~/.ldk-server/config.toml, so you can just run the daemon and CLI without having to specify any flags. We make sure to separate out data by network so we don't conflict anywhere and accidentally lose anything important --- Cargo.lock | 2 + ldk-server-cli/Cargo.toml | 2 + ldk-server-cli/src/config.rs | 81 +++++++++++++++++ ldk-server-cli/src/main.rs | 78 +++++++++++++--- ldk-server/ldk-server-config.toml | 3 +- ldk-server/src/main.rs | 146 +++++++++++++++++++++++++----- ldk-server/src/util/config.rs | 21 ++--- 7 files changed, 276 insertions(+), 57 deletions(-) create mode 100644 ldk-server-cli/src/config.rs diff --git a/Cargo.lock b/Cargo.lock index a771737..bb849fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1760,10 +1760,12 @@ name = "ldk-server-cli" version = "0.1.0" dependencies = [ "clap", + "hex-conservative 0.2.1", "ldk-server-client", "serde", "serde_json", "tokio", + "toml", ] [[package]] diff --git a/ldk-server-cli/Cargo.toml b/ldk-server-cli/Cargo.toml index bc77d1d..2ac0c4c 100644 --- a/ldk-server-cli/Cargo.toml +++ b/ldk-server-cli/Cargo.toml @@ -6,6 +6,8 @@ edition = "2021" [dependencies] ldk-server-client = { path = "../ldk-server-client", features = ["serde"] } clap = { version = "4.0.5", default-features = false, features = ["derive", "std", "error-context", "suggestions", "help"] } +hex-conservative = { version = "0.2", default-features = false, features = ["std"] } tokio = { version = "1.38.0", default-features = false, features = ["rt-multi-thread", "macros"] } serde = "1.0" serde_json = "1.0" +toml = { version = "0.8", default-features = false, features = ["parse"] } diff --git a/ldk-server-cli/src/config.rs b/ldk-server-cli/src/config.rs new file mode 100644 index 0000000..3158ac5 --- /dev/null +++ b/ldk-server-cli/src/config.rs @@ -0,0 +1,81 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +const DEFAULT_CONFIG_FILE: &str = "config.toml"; +const DEFAULT_CERT_FILE: &str = "tls.crt"; +const API_KEY_FILE: &str = "api_key"; + +pub fn get_default_data_dir() -> Option { + #[cfg(target_os = "macos")] + { + #[allow(deprecated)] // todo can remove once we update MSRV to 1.87+ + std::env::home_dir().map(|home| home.join("Library/Application Support/ldk-server")) + } + #[cfg(target_os = "windows")] + { + std::env::var("APPDATA").ok().map(|appdata| PathBuf::from(appdata).join("ldk-server")) + } + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + { + #[allow(deprecated)] // todo can remove once we update MSRV to 1.87+ + std::env::home_dir().map(|home| home.join(".ldk-server")) + } +} + +pub fn get_default_config_path() -> Option { + get_default_data_dir().map(|dir| dir.join(DEFAULT_CONFIG_FILE)) +} + +pub fn get_default_cert_path() -> Option { + get_default_data_dir().map(|path| path.join(DEFAULT_CERT_FILE)) +} + +pub fn get_default_api_key_path(network: &str) -> Option { + get_default_data_dir().map(|path| path.join(network).join(API_KEY_FILE)) +} + +#[derive(Debug, Deserialize)] +pub struct Config { + pub node: NodeConfig, + pub tls: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct TlsConfig { + pub cert_path: Option, +} + +#[derive(Debug, Deserialize)] +pub struct NodeConfig { + pub rest_service_address: String, + network: String, +} + +impl Config { + pub fn network(&self) -> Result { + match self.node.network.as_str() { + "bitcoin" | "mainnet" => Ok("bitcoin".to_string()), + "testnet" => Ok("testnet".to_string()), + "testnet4" => Ok("testnet4".to_string()), + "signet" => Ok("signet".to_string()), + "regtest" => Ok("regtest".to_string()), + other => Err(format!("Unsupported network: {other}")), + } + } +} + +pub fn load_config(path: &PathBuf) -> Result { + let contents = std::fs::read_to_string(path) + .map_err(|e| format!("Failed to read config file '{}': {}", path.display(), e))?; + toml::from_str(&contents) + .map_err(|e| format!("Failed to parse config file '{}': {}", path.display(), e)) +} diff --git a/ldk-server-cli/src/main.rs b/ldk-server-cli/src/main.rs index 504b6da..fae510b 100644 --- a/ldk-server-cli/src/main.rs +++ b/ldk-server-cli/src/main.rs @@ -8,6 +8,10 @@ // licenses. use clap::{Parser, Subcommand}; +use config::{ + get_default_api_key_path, get_default_cert_path, get_default_config_path, load_config, +}; +use hex_conservative::DisplayHex; use ldk_server_client::client::LdkServerClient; use ldk_server_client::error::LdkServerError; use ldk_server_client::error::LdkServerErrorCode::{ @@ -28,8 +32,10 @@ use ldk_server_client::ldk_server_protos::types::{ RouteParametersConfig, }; use serde::Serialize; +use std::path::PathBuf; use types::CliListPaymentsResponse; +mod config; mod types; // Having these default values as constants in the Proto file and @@ -43,19 +49,25 @@ const DEFAULT_EXPIRY_SECS: u32 = 86_400; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Cli { - #[arg(short, long, default_value = "localhost:3000")] - base_url: String, + #[arg(short, long, help = "Base URL of the server. If not provided, reads from config file")] + base_url: Option, - #[arg(short, long, required(true))] - api_key: String, + #[arg( + short, + long, + help = "API key for authentication. Defaults by reading ~/.ldk-server/[network]/api_key" + )] + api_key: Option, #[arg( short, long, - required(true), - help = "Path to the server's TLS certificate file (PEM format). Found at /tls.crt" + help = "Path to the server's TLS certificate file (PEM format). Defaults to ~/.ldk-server/tls.crt" )] - tls_cert: String, + tls_cert: Option, + + #[arg(short, long, help = "Path to config file. Defaults to ~/.ldk-server/config.toml")] + config: Option, #[command(subcommand)] command: Commands, @@ -226,18 +238,54 @@ enum Commands { async fn main() { let cli = Cli::parse(); - // Load server certificate for TLS verification - let server_cert_pem = std::fs::read(&cli.tls_cert).unwrap_or_else(|e| { - eprintln!("Failed to read server certificate file '{}': {}", cli.tls_cert, e); - std::process::exit(1); - }); + 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 client = - LdkServerClient::new(cli.base_url, cli.api_key, &server_cert_pem).unwrap_or_else(|e| { - eprintln!("Failed to create client: {e}"); + // Get API key from argument, then from api_key file + let api_key = cli + .api_key + .or_else(|| { + // Try to read from api_key file based on network (file contains raw bytes) + let network = config.as_ref().and_then(|c| c.network().ok()).unwrap_or("bitcoin".to_string()); + get_default_api_key_path(&network) + .and_then(|path| std::fs::read(&path).ok()) + .map(|bytes| bytes.to_lower_hex_string()) + }) + .unwrap_or_else(|| { + eprintln!("API key not provided. Use --api-key or ensure the api_key file exists at ~/.ldk-server/[network]/api_key"); std::process::exit(1); }); + // Get base URL from argument then from config file + let base_url = + cli.base_url.or_else(|| config.as_ref().map(|c| c.node.rest_service_address.clone())) + .unwrap_or_else(|| { + eprintln!("Base URL not provided. Use --base-url or ensure config file exists at ~/.ldk-server/config.toml"); + std::process::exit(1); + }); + + // Get TLS cert path from argument, then from config file, then try default location + let tls_cert_path = cli.tls_cert.map(PathBuf::from).or_else(|| { + config + .as_ref() + .and_then(|c| c.tls.as_ref().and_then(|t| t.cert_path.as_ref().map(PathBuf::from))) + .or_else(get_default_cert_path) + }) + .unwrap_or_else(|| { + eprintln!("TLS cert path not provided. Use --tls-cert or ensure config file exists at ~/.ldk-server/config.toml"); + std::process::exit(1); + }); + + let server_cert_pem = std::fs::read(&tls_cert_path).unwrap_or_else(|e| { + eprintln!("Failed to read server certificate file '{}': {}", tls_cert_path.display(), e); + std::process::exit(1); + }); + + let client = LdkServerClient::new(base_url, api_key, &server_cert_pem).unwrap_or_else(|e| { + eprintln!("Failed to create client: {e}"); + std::process::exit(1); + }); + match cli.command { Commands::GetNodeInfo => { handle_response_result::<_, GetNodeInfoResponse>( diff --git a/ldk-server/ldk-server-config.toml b/ldk-server/ldk-server-config.toml index 0c2658e..3c31cbc 100644 --- a/ldk-server/ldk-server-config.toml +++ b/ldk-server/ldk-server-config.toml @@ -4,11 +4,10 @@ network = "regtest" # Bitcoin network to use listening_addresses = ["localhost:3001"] # Lightning node listening addresses announcement_addresses = ["54.3.7.81:3001"] # Lightning node announcement addresses rest_service_address = "127.0.0.1:3002" # LDK Server REST address -api_key = "your-secret-api-key" # API key for authenticating REST requests # Storage settings [storage.disk] -dir_path = "/tmp/ldk-server/" # Path for LDK and BDK data persistence +dir_path = "/tmp/ldk-server/" # Path for LDK and BDK data persistence, optional, defaults to ~/.ldk-server/ [log] level = "Debug" # Log level (Error, Warn, Info, Debug, Trace) diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index 038b219..acc1a9b 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -13,6 +13,7 @@ mod service; mod util; use std::fs; +use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; @@ -20,6 +21,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use hex::DisplayHex; use hyper::server::conn::http1; use hyper_util::rt::TokioIo; +use ldk_node::bitcoin::Network; use ldk_node::config::Config; use ldk_node::entropy::NodeEntropy; use ldk_node::lightning::ln::channelmanager::PaymentId; @@ -27,7 +29,7 @@ use ldk_node::{Builder, Event, Node}; use ldk_server_protos::events; use ldk_server_protos::events::{event_envelope, EventEnvelope}; use ldk_server_protos::types::Payment; -use log::{error, info}; +use log::{debug, error, info}; use prost::Message; use rand::Rng; use tokio::net::TcpListener; @@ -51,29 +53,65 @@ use crate::util::logger::ServerLogger; use crate::util::proto_adapter::{forwarded_payment_to_proto, payment_to_proto}; use crate::util::tls::get_or_generate_tls_config; -const USAGE_GUIDE: &str = "Usage: ldk-server "; +const DEFAULT_CONFIG_FILE: &str = "config.toml"; +const API_KEY_FILE: &str = "api_key"; + +fn get_default_data_dir() -> Option { + #[cfg(target_os = "macos")] + { + #[allow(deprecated)] // todo can remove once we update MSRV to 1.87+ + std::env::home_dir().map(|home| home.join("Library/Application Support/ldk-server")) + } + #[cfg(target_os = "windows")] + { + std::env::var("APPDATA").ok().map(|appdata| PathBuf::from(appdata).join("ldk-server")) + } + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + { + #[allow(deprecated)] // todo can remove once we update MSRV to 1.87+ + std::env::home_dir().map(|home| home.join(".ldk-server")) + } +} + +fn get_default_config_path() -> Option { + get_default_data_dir().map(|data_dir| data_dir.join(DEFAULT_CONFIG_FILE)) +} + +const USAGE_GUIDE: &str = "Usage: ldk-server [config_path] + +If no config path is provided, ldk-server will look for a config file at: + Linux: ~/.ldk-server/config.toml + macOS: ~/Library/Application Support/ldk-server/config.toml + Windows: %APPDATA%\\ldk-server\\config.toml"; fn main() { let args: Vec = std::env::args().collect(); - if args.len() < 2 { - eprintln!("{USAGE_GUIDE}"); - std::process::exit(-1); - } - - let arg = args[1].as_str(); - if arg == "-h" || arg == "--help" { - println!("{}", USAGE_GUIDE); - std::process::exit(0); - } + let config_path: PathBuf = if args.len() < 2 { + match get_default_config_path() { + Some(path) => path, + None => { + eprintln!("Unable to determine home directory for default config path."); + eprintln!("{USAGE_GUIDE}"); + std::process::exit(-1); + }, + } + } else { + let arg = args[1].as_str(); + if arg == "-h" || arg == "--help" { + println!("{USAGE_GUIDE}"); + std::process::exit(0); + } + PathBuf::from(arg) + }; - if fs::File::open(arg).is_err() { - eprintln!("Unable to access configuration file."); + if fs::File::open(&config_path).is_err() { + eprintln!("Unable to access configuration file: {}", config_path.display()); std::process::exit(-1); } let mut ldk_node_config = Config::default(); - let config_file = match load_config(Path::new(arg)) { + let config_file = match load_config(&config_path) { Ok(config) => config, Err(e) => { eprintln!("Invalid configuration file: {}", e); @@ -81,13 +119,38 @@ fn main() { }, }; + let storage_dir: PathBuf = match config_file.storage_dir_path { + None => { + let default = get_default_data_dir(); + match default { + Some(path) => { + info!("No storage_dir_path configured, defaulting to {}", path.display()); + path + }, + None => { + eprintln!("Unable to determine home directory for default storage path."); + std::process::exit(-1); + }, + } + }, + Some(configured_path) => PathBuf::from(configured_path), + }; + + let network_dir: PathBuf = match config_file.network { + Network::Bitcoin => storage_dir.join("bitcoin"), + Network::Testnet => storage_dir.join("testnet"), + Network::Testnet4 => storage_dir.join("testnet4"), + Network::Signet => storage_dir.join("signet"), + Network::Regtest => storage_dir.join("regtest"), + }; + let log_file_path = config_file.log_file_path.map(PathBuf::from).unwrap_or_else(|| { - let mut default_log_path = PathBuf::from(&config_file.storage_dir_path); + let mut default_log_path = network_dir.clone(); default_log_path.push("ldk-server.log"); default_log_path }); - if log_file_path == PathBuf::from(&config_file.storage_dir_path) { + if log_file_path == storage_dir || log_file_path == network_dir { eprintln!("Log file path cannot be the same as storage directory path."); std::process::exit(-1); } @@ -100,7 +163,15 @@ fn main() { }, }; - ldk_node_config.storage_dir_path = config_file.storage_dir_path.clone(); + let api_key = match load_or_generate_api_key(&network_dir) { + Ok(key) => key, + Err(e) => { + eprintln!("Failed to load or generate API key: {e}"); + std::process::exit(-1); + }, + }; + + ldk_node_config.storage_dir_path = network_dir.to_str().unwrap().to_string(); ldk_node_config.listening_addresses = config_file.listening_addrs; ldk_node_config.announcement_addresses = config_file.announcement_addrs; ldk_node_config.network = config_file.network; @@ -148,7 +219,7 @@ fn main() { builder.set_runtime(runtime.handle().clone()); - let seed_path = format!("{}/keys_seed", config_file.storage_dir_path); + let seed_path = storage_dir.join("keys_seed").to_str().unwrap().to_string(); let node_entropy = match NodeEntropy::from_seed_path(seed_path) { Ok(entropy) => entropy, Err(e) => { @@ -165,15 +236,14 @@ fn main() { }, }; - let paginated_store: Arc = Arc::new( - match SqliteStore::new(PathBuf::from(&config_file.storage_dir_path), None, None) { + let paginated_store: Arc = + Arc::new(match SqliteStore::new(network_dir.clone(), None, None) { Ok(store) => store, Err(e) => { error!("Failed to create SqliteStore: {e:?}"); std::process::exit(-1); }, - }, - ); + }); #[cfg(not(feature = "events-rabbitmq"))] let event_publisher: Arc = @@ -227,7 +297,7 @@ fn main() { let server_config = match get_or_generate_tls_config( config_file.tls_config, - &config_file.storage_dir_path, + storage_dir.to_str().unwrap(), ) { Ok(config) => config, Err(e) => { @@ -380,7 +450,7 @@ fn main() { res = rest_svc_listener.accept() => { match res { Ok((stream, _)) => { - let node_service = NodeService::new(Arc::clone(&node), Arc::clone(&paginated_store), config_file.api_key.clone()); + let node_service = NodeService::new(Arc::clone(&node), Arc::clone(&paginated_store), api_key.clone()); let acceptor = tls_acceptor.clone(); runtime.spawn(async move { match acceptor.accept(stream).await { @@ -465,3 +535,29 @@ fn upsert_payment_details( }, } } + +/// Loads the API key from a file, or generates a new one if it doesn't exist. +/// The API key file is stored with 0400 permissions (read-only for owner). +fn load_or_generate_api_key(storage_dir: &Path) -> std::io::Result { + let api_key_path = storage_dir.join(API_KEY_FILE); + + if api_key_path.exists() { + let key_bytes = fs::read(&api_key_path)?; + Ok(key_bytes.to_lower_hex_string()) + } else { + // Generate a 32-byte random API key + let mut rng = rand::thread_rng(); + let mut key_bytes = [0u8; 32]; + rng.fill(&mut key_bytes); + + // Write the raw bytes to the file + fs::write(&api_key_path, key_bytes)?; + + // Set permissions to 0400 (read-only for owner) + let permissions = fs::Permissions::from_mode(0o400); + fs::set_permissions(&api_key_path, permissions)?; + + debug!("Generated new API key at {}", api_key_path.display()); + Ok(key_bytes.to_lower_hex_string()) + } +} diff --git a/ldk-server/src/util/config.rs b/ldk-server/src/util/config.rs index 2969084..3824a82 100644 --- a/ldk-server/src/util/config.rs +++ b/ldk-server/src/util/config.rs @@ -26,10 +26,9 @@ pub struct Config { pub announcement_addrs: Option>, pub alias: Option, pub network: Network, - pub api_key: String, pub tls_config: Option, pub rest_service_addr: SocketAddr, - pub storage_dir_path: String, + pub storage_dir_path: Option, pub chain_source: ChainSource, pub rabbitmq_connection_string: String, pub rabbitmq_exchange_name: String, @@ -194,8 +193,7 @@ impl TryFrom for Config { network: toml_config.node.network, alias, rest_service_addr, - api_key: toml_config.node.api_key, - storage_dir_path: toml_config.storage.disk.dir_path, + storage_dir_path: toml_config.storage.and_then(|s| s.disk.and_then(|d| d.dir_path)), chain_source, rabbitmq_connection_string, rabbitmq_exchange_name, @@ -211,7 +209,7 @@ impl TryFrom for Config { #[derive(Deserialize, Serialize)] pub struct TomlConfig { node: NodeConfig, - storage: StorageConfig, + storage: Option, bitcoind: Option, electrum: Option, esplora: Option, @@ -228,17 +226,16 @@ struct NodeConfig { announcement_addresses: Option>, rest_service_address: String, alias: Option, - api_key: String, } #[derive(Deserialize, Serialize)] struct StorageConfig { - disk: DiskConfig, + disk: Option, } #[derive(Deserialize, Serialize)] struct DiskConfig { - dir_path: String, + dir_path: Option, } #[derive(Deserialize, Serialize)] @@ -365,7 +362,6 @@ mod tests { announcement_addresses = ["54.3.7.81:3001"] rest_service_address = "127.0.0.1:3002" alias = "LDK Server" - api_key = "test_api_key" [tls] cert_path = "/path/to/tls.crt" @@ -411,8 +407,7 @@ mod tests { alias: Some(NodeAlias(bytes)), network: Network::Regtest, rest_service_addr: SocketAddr::from_str("127.0.0.1:3002").unwrap(), - api_key: "test_api_key".to_string(), - storage_dir_path: "/tmp".to_string(), + storage_dir_path: Some("/tmp".to_string()), tls_config: Some(TlsConfig { cert_path: Some("/path/to/tls.crt".to_string()), key_path: Some("/path/to/tls.key".to_string()), @@ -444,7 +439,6 @@ mod tests { assert_eq!(config.alias, expected.alias); assert_eq!(config.network, expected.network); assert_eq!(config.rest_service_addr, expected.rest_service_addr); - assert_eq!(config.api_key, expected.api_key); assert_eq!(config.storage_dir_path, expected.storage_dir_path); assert_eq!(config.tls_config, expected.tls_config); let ChainSource::Esplora { server_url } = config.chain_source else { @@ -468,7 +462,6 @@ mod tests { network = "regtest" rest_service_address = "127.0.0.1:3002" alias = "LDK Server" - api_key = "test_api_key" [storage.disk] dir_path = "/tmp" @@ -512,7 +505,6 @@ mod tests { network = "regtest" rest_service_address = "127.0.0.1:3002" alias = "LDK Server" - api_key = "test_api_key" [storage.disk] dir_path = "/tmp" @@ -560,7 +552,6 @@ mod tests { network = "regtest" rest_service_address = "127.0.0.1:3002" alias = "LDK Server" - api_key = "test_api_key" [storage.disk] dir_path = "/tmp"