Skip to content

Commit 27f36c4

Browse files
committed
Require HTTP Basic Auth for all RPC/CLI
Adds basic auth to protect the RPC from potential attackers.
1 parent e9f047b commit 27f36c4

File tree

9 files changed

+238
-22
lines changed

9 files changed

+238
-22
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,36 +21,42 @@ a Lightning node while exposing a robust, language-agnostic API via [Protocol Bu
2121
- Built on top of LDK-Node, leveraging the modular, reliable, and high-performance architecture of LDK.
2222

2323
- **Effortless Integration**:
24-
- Ideal for embedding Lightning functionality into payment processors, self-hosted nodes, custodial wallets, or other Lightning-enabled
24+
- Ideal for embedding Lightning functionality into payment processors, self-hosted nodes, custodial wallets, or
25+
other Lightning-enabled
2526
applications.
2627

2728
### Project Status
2829

2930
🚧 **Work in Progress**:
31+
3032
- **APIs Under Development**: Expect breaking changes as the project evolves.
3133
- **Potential Bugs and Inconsistencies**: While progress is being made toward stability, unexpected behavior may occur.
32-
- **Improved Logging and Error Handling Coming Soon**: Current error handling is rudimentary (specially for CLI), and usability improvements are actively being worked on.
34+
- **Improved Logging and Error Handling Coming Soon**: Current error handling is rudimentary (specially for CLI), and
35+
usability improvements are actively being worked on.
3336
- **Pending Testing**: Not tested, hence don't use it for production!
3437

3538
We welcome your feedback and contributions to help shape the future of LDK Server!
3639

37-
3840
### Configuration
41+
3942
Refer `./ldk-server/ldk-server-config.toml` to see available configuration options.
4043

4144
### Building
45+
4246
```
4347
git clone https://github.com/lightningdevkit/ldk-server.git
4448
cargo build
4549
```
4650

4751
### Running
52+
4853
```
4954
cargo run --bin ldk-server ./ldk-server/ldk-server-config.toml
5055
```
5156

5257
Interact with the node using CLI:
58+
5359
```
54-
./target/debug/ldk-server-cli -b localhost:3002 onchain-receive # To generate onchain-receive address.
55-
./target/debug/ldk-server-cli -b localhost:3002 help # To print help/available commands.
60+
./target/debug/ldk-server-cli -b localhost:3002 -u user -p pass onchain-receive # To generate onchain-receive address.
61+
./target/debug/ldk-server-cli -b localhost:3002 -u user -p pass help # To print help/available commands.
5662
```

ldk-server-cli/src/main.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ struct Cli {
4343
#[arg(short, long, default_value = "localhost:3000")]
4444
base_url: String,
4545

46+
#[arg(short, long)]
47+
username: String,
48+
49+
#[arg(short, long)]
50+
password: String,
51+
4652
#[command(subcommand)]
4753
command: Commands,
4854
}
@@ -208,7 +214,7 @@ enum Commands {
208214
#[tokio::main]
209215
async fn main() {
210216
let cli = Cli::parse();
211-
let client = LdkServerClient::new(cli.base_url);
217+
let client = LdkServerClient::new(cli.base_url, cli.username, cli.password);
212218

213219
match cli.command {
214220
Commands::GetNodeInfo => {

ldk-server-client/src/client.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,14 @@ const APPLICATION_OCTET_STREAM: &str = "application/octet-stream";
4040
pub struct LdkServerClient {
4141
base_url: String,
4242
client: Client,
43+
auth_credentials: (String, String),
4344
}
4445

4546
impl LdkServerClient {
4647
/// Constructs a [`LdkServerClient`] using `base_url` as the ldk-server endpoint.
47-
pub fn new(base_url: String) -> Self {
48-
Self { base_url, client: Client::new() }
48+
/// `username` and `password` are used for basic authentication.
49+
pub fn new(base_url: String, username: String, password: String) -> Self {
50+
Self { base_url, client: Client::new(), auth_credentials: (username, password) }
4951
}
5052

5153
/// Retrieve the latest node info like `node_id`, `current_best_block` etc.
@@ -196,10 +198,12 @@ impl LdkServerClient {
196198
&self, request: &Rq, url: &str,
197199
) -> Result<Rs, LdkServerError> {
198200
let request_body = request.encode_to_vec();
201+
let (username, password) = &self.auth_credentials;
199202
let response_raw = self
200203
.client
201204
.post(url)
202205
.header(CONTENT_TYPE, APPLICATION_OCTET_STREAM)
206+
.basic_auth(username, Some(password))
203207
.body(request_body)
204208
.send()
205209
.await

ldk-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ async-trait = { version = "0.1.85", default-features = false }
2020
toml = { version = "0.8.9", default-features = false, features = ["parse"] }
2121
chrono = { version = "0.4", default-features = false, features = ["clock"] }
2222
log = "0.4.28"
23+
base64 = { version = "0.21", default-features = false, features = ["std"] }
2324

2425
# Required for RabittMQ based EventPublisher. Only enabled for `events-rabbitmq` feature.
2526
lapin = { version = "2.4.0", features = ["rustls"], default-features = false, optional = true }

ldk-server/ldk-server-config.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ dir_path = "/tmp/ldk-server/" # Path for LDK and BDK data persis
1212
level = "Debug" # Log level (Error, Warn, Info, Debug, Trace)
1313
file_path = "/tmp/ldk-server/ldk-server.log" # Log file path
1414

15+
# HTTP Basic Authentication (REQUIRED)
16+
[auth]
17+
username = "your-username"
18+
password = "your-password"
19+
1520
# Must set either bitcoind or esplora settings, but not both
1621

1722
# Bitcoin Core settings

ldk-server/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ fn main() {
351351
match res {
352352
Ok((stream, _)) => {
353353
let io_stream = TokioIo::new(stream);
354-
let node_service = NodeService::new(Arc::clone(&node), Arc::clone(&paginated_store));
354+
let node_service = NodeService::new(Arc::clone(&node), Arc::clone(&paginated_store), config_file.auth_config.clone());
355355
runtime.spawn(async move {
356356
if let Err(err) = http1::Builder::new().serve_connection(io_stream, node_service).await {
357357
error!("Failed to serve connection: {}", err);

ldk-server/src/service.rs

Lines changed: 155 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
// You may not use this file except in accordance with one or both of these
88
// licenses.
99

10+
use ldk_node::bitcoin::base64::{self, Engine};
1011
use ldk_node::Node;
1112

1213
use http_body_util::{BodyExt, Full, Limited};
1314
use hyper::body::{Bytes, Incoming};
15+
use hyper::header::AUTHORIZATION;
1416
use hyper::service::Service;
1517
use hyper::{Request, Response, StatusCode};
1618

@@ -30,7 +32,7 @@ use crate::api::bolt12_receive::handle_bolt12_receive_request;
3032
use crate::api::bolt12_send::handle_bolt12_send_request;
3133
use crate::api::close_channel::{handle_close_channel_request, handle_force_close_channel_request};
3234
use crate::api::error::LdkServerError;
33-
use crate::api::error::LdkServerErrorCode::InvalidRequestError;
35+
use crate::api::error::LdkServerErrorCode::{AuthError, InvalidRequestError};
3436
use crate::api::get_balances::handle_get_balances_request;
3537
use crate::api::get_node_info::handle_get_node_info_request;
3638
use crate::api::get_payment_details::handle_get_payment_details_request;
@@ -43,6 +45,7 @@ use crate::api::open_channel::handle_open_channel;
4345
use crate::api::splice_channel::{handle_splice_in_request, handle_splice_out_request};
4446
use crate::api::update_channel_config::handle_update_channel_config_request;
4547
use crate::io::persist::paginated_kv_store::PaginatedKVStore;
48+
use crate::util::config::BasicAuthConfig;
4649
use crate::util::proto_adapter::to_error_response;
4750
use std::future::Future;
4851
use std::pin::Pin;
@@ -56,11 +59,44 @@ const MAX_BODY_SIZE: usize = 10 * 1024 * 1024;
5659
pub struct NodeService {
5760
node: Arc<Node>,
5861
paginated_kv_store: Arc<dyn PaginatedKVStore>,
62+
auth_config: BasicAuthConfig,
5963
}
6064

6165
impl NodeService {
62-
pub(crate) fn new(node: Arc<Node>, paginated_kv_store: Arc<dyn PaginatedKVStore>) -> Self {
63-
Self { node, paginated_kv_store }
66+
pub(crate) fn new(
67+
node: Arc<Node>, paginated_kv_store: Arc<dyn PaginatedKVStore>,
68+
auth_config: BasicAuthConfig,
69+
) -> Self {
70+
Self { node, paginated_kv_store, auth_config }
71+
}
72+
}
73+
74+
fn validate_auth<B>(req: &Request<B>, auth_config: &BasicAuthConfig) -> Result<(), LdkServerError> {
75+
let auth_header = req
76+
.headers()
77+
.get(AUTHORIZATION)
78+
.and_then(|v| v.to_str().ok())
79+
.ok_or_else(|| LdkServerError::new(AuthError, "Missing Authorization header"))?;
80+
81+
let encoded = auth_header
82+
.strip_prefix("Basic ")
83+
.ok_or_else(|| LdkServerError::new(AuthError, "Invalid Authorization header format"))?;
84+
85+
let decoded = base64::engine::general_purpose::STANDARD
86+
.decode(encoded)
87+
.map_err(|_| LdkServerError::new(AuthError, "Invalid base64 encoding"))?;
88+
89+
let credentials = std::str::from_utf8(&decoded)
90+
.map_err(|_| LdkServerError::new(AuthError, "Invalid credentials format"))?;
91+
92+
let (username, password) = credentials
93+
.split_once(':')
94+
.ok_or_else(|| LdkServerError::new(AuthError, "Invalid credentials format"))?;
95+
96+
if username == auth_config.username && password == auth_config.password {
97+
Ok(())
98+
} else {
99+
Err(LdkServerError::new(AuthError, "Invalid credentials"))
64100
}
65101
}
66102

@@ -75,6 +111,18 @@ impl Service<Request<Incoming>> for NodeService {
75111
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
76112

77113
fn call(&self, req: Request<Incoming>) -> Self::Future {
114+
// Validate authentication
115+
if let Err(e) = validate_auth(&req, &self.auth_config) {
116+
let (error_response, status_code) = to_error_response(e);
117+
return Box::pin(async move {
118+
Ok(Response::builder()
119+
.status(status_code)
120+
.body(Full::new(Bytes::from(error_response.encode_to_vec())))
121+
// unwrap safety: body only errors when previous chained calls failed.
122+
.unwrap())
123+
});
124+
}
125+
78126
let context = Context {
79127
node: Arc::clone(&self.node),
80128
paginated_kv_store: Arc::clone(&self.paginated_kv_store),
@@ -189,3 +237,107 @@ async fn handle_request<
189237
},
190238
}
191239
}
240+
241+
#[cfg(test)]
242+
mod tests {
243+
use super::*;
244+
use hyper::header::AUTHORIZATION;
245+
246+
fn create_test_request(auth_header: Option<String>) -> Request<()> {
247+
let mut builder = Request::builder();
248+
if let Some(header) = auth_header {
249+
builder = builder.header(AUTHORIZATION, header);
250+
}
251+
builder.body(()).unwrap()
252+
}
253+
254+
#[test]
255+
fn test_validate_auth_success() {
256+
let auth_config =
257+
BasicAuthConfig { username: "testuser".to_string(), password: "testpass".to_string() };
258+
259+
// Create a valid Basic Auth header
260+
let credentials = format!("{}:{}", auth_config.username, auth_config.password);
261+
let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
262+
let auth_header = format!("Basic {encoded}");
263+
264+
let req = create_test_request(Some(auth_header));
265+
266+
let result = validate_auth(&req, &auth_config);
267+
assert!(result.is_ok());
268+
}
269+
270+
#[test]
271+
fn test_validate_auth_wrong_password() {
272+
let auth_config =
273+
BasicAuthConfig { username: "testuser".to_string(), password: "testpass".to_string() };
274+
275+
// Wrong password
276+
let credentials = format!("{}:wrongpass", auth_config.username);
277+
let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
278+
let auth_header = format!("Basic {encoded}");
279+
280+
let req = create_test_request(Some(auth_header));
281+
282+
let result = validate_auth(&req, &auth_config);
283+
assert!(result.is_err());
284+
assert_eq!(result.unwrap_err().error_code, AuthError);
285+
}
286+
287+
#[test]
288+
fn test_validate_auth_wrong_username() {
289+
let auth_config =
290+
BasicAuthConfig { username: "testuser".to_string(), password: "testpass".to_string() };
291+
292+
// Wrong username
293+
let credentials = format!("wronguser:{}", auth_config.password);
294+
let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
295+
let auth_header = format!("Basic {encoded}");
296+
297+
let req = create_test_request(Some(auth_header));
298+
299+
let result = validate_auth(&req, &auth_config);
300+
assert!(result.is_err());
301+
assert_eq!(result.unwrap_err().error_code, AuthError);
302+
}
303+
304+
#[test]
305+
fn test_validate_auth_missing_header() {
306+
let auth_config =
307+
BasicAuthConfig { username: "testuser".to_string(), password: "testpass".to_string() };
308+
309+
let req = create_test_request(None);
310+
311+
let result = validate_auth(&req, &auth_config);
312+
assert!(result.is_err());
313+
assert_eq!(result.unwrap_err().error_code, AuthError);
314+
}
315+
316+
#[test]
317+
fn test_validate_auth_invalid_format() {
318+
let auth_config =
319+
BasicAuthConfig { username: "testuser".to_string(), password: "testpass".to_string() };
320+
321+
let credentials = format!("{}:{}", auth_config.username, auth_config.password);
322+
let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
323+
// Missing "Basic " prefix
324+
let req = create_test_request(Some(encoded));
325+
326+
let result = validate_auth(&req, &auth_config);
327+
assert!(result.is_err());
328+
assert_eq!(result.unwrap_err().error_code, AuthError);
329+
}
330+
331+
#[test]
332+
fn test_validate_auth_invalid_base64() {
333+
let auth_config =
334+
BasicAuthConfig { username: "testuser".to_string(), password: "testpass".to_string() };
335+
336+
// Invalid base64
337+
let req = create_test_request(Some("Basic not-valid-base64!".to_string()));
338+
339+
let result = validate_auth(&req, &auth_config);
340+
assert!(result.is_err());
341+
assert_eq!(result.unwrap_err().error_code, AuthError);
342+
}
343+
}

0 commit comments

Comments
 (0)