@@ -14,9 +14,11 @@ use e2e_tests::{
1414 find_available_port, mine_and_sync, run_cli, run_cli_raw, setup_funded_channel,
1515 wait_for_onchain_balance, LdkServerConfig , LdkServerHandle , TestBitcoind ,
1616} ;
17- use ldk_node:: lightning:: ln:: msgs:: SocketAddress ;
1817use hex_conservative:: { DisplayHex , FromHex } ;
18+ use ldk_node:: bitcoin:: hashes:: hmac:: { Hmac , HmacEngine } ;
19+ use ldk_node:: bitcoin:: hashes:: HashEngine ;
1920use ldk_node:: bitcoin:: hashes:: { sha256, Hash } ;
21+ use ldk_node:: lightning:: ln:: msgs:: SocketAddress ;
2022use ldk_node:: lightning:: offers:: offer:: Offer ;
2123use ldk_node:: lightning_invoice:: Bolt11Invoice ;
2224use ldk_server_client:: ldk_server_protos:: api:: {
@@ -26,6 +28,35 @@ use ldk_server_client::ldk_server_protos::events::event_envelope::Event;
2628use ldk_server_client:: ldk_server_protos:: types:: {
2729 bolt11_invoice_description, Bolt11InvoiceDescription ,
2830} ;
31+ use ldk_server_protos:: endpoints:: GET_NODE_INFO_PATH ;
32+ use reqwest:: { header:: HeaderMap , Certificate , Client } ;
33+
34+ fn compute_auth_header ( api_key : & str ) -> String {
35+ let timestamp =
36+ std:: time:: SystemTime :: now ( ) . duration_since ( std:: time:: UNIX_EPOCH ) . unwrap ( ) . as_secs ( ) ;
37+ let mut hmac_engine: HmacEngine < sha256:: Hash > = HmacEngine :: new ( api_key. as_bytes ( ) ) ;
38+ hmac_engine. input ( & timestamp. to_be_bytes ( ) ) ;
39+ let hmac = Hmac :: < sha256:: Hash > :: from_engine ( hmac_engine) ;
40+ format ! ( "HMAC {timestamp}:{hmac}" )
41+ }
42+
43+ async fn raw_grpc_request (
44+ server : & LdkServerHandle , path : & str , extra_headers : HeaderMap ,
45+ ) -> reqwest:: Response {
46+ let cert = Certificate :: from_pem ( & std:: fs:: read ( & server. tls_cert_path ) . unwrap ( ) ) . unwrap ( ) ;
47+ let client = Client :: builder ( ) . use_rustls_tls ( ) . add_root_certificate ( cert) . build ( ) . unwrap ( ) ;
48+
49+ let mut request = client
50+ . post ( format ! ( "https://{}{}" , server. base_url( ) , path) )
51+ . header ( "content-type" , "application/grpc+proto" )
52+ . header ( "te" , "trailers" )
53+ . header ( "x-auth" , compute_auth_header ( & server. api_key ) ) ;
54+ for ( name, value) in & extra_headers {
55+ request = request. header ( name, value) ;
56+ }
57+
58+ request. body ( vec ! [ 0 , 0 , 0 , 0 , 0 ] ) . send ( ) . await . unwrap ( )
59+ }
2960
3061#[ tokio:: test]
3162async fn test_cli_get_node_info ( ) {
@@ -215,31 +246,21 @@ async fn test_cli_decode_invoice() {
215246 // serde_json escapes control chars (U+0000–U+001F) as \uXXXX in JSON.
216247 let desc_with_ansi = "pay me\x1b [31m RED \x1b [0m" ;
217248 let output_ansi = run_cli ( & server, & [ "bolt11-receive" , "-d" , desc_with_ansi] ) ;
218- let raw_decoded = run_cli_raw (
219- & server,
220- & [ "decode-invoice" , output_ansi[ "invoice" ] . as_str ( ) . unwrap ( ) ] ,
221- ) ;
222- assert ! (
223- !raw_decoded. contains( '\x1b' ) ,
224- "Raw CLI output must not contain ANSI escape bytes"
225- ) ;
249+ let raw_decoded =
250+ run_cli_raw ( & server, & [ "decode-invoice" , output_ansi[ "invoice" ] . as_str ( ) . unwrap ( ) ] ) ;
251+ assert ! ( !raw_decoded. contains( '\x1b' ) , "Raw CLI output must not contain ANSI escape bytes" ) ;
226252
227253 // Test that Unicode bidi override characters in the description are escaped
228254 // (sanitize_for_terminal replaces them with \uXXXX in CLI output)
229255 let desc_with_bidi = "pay me\u{202E} evil" ;
230256 let output_bidi = run_cli ( & server, & [ "bolt11-receive" , "-d" , desc_with_bidi] ) ;
231- let raw_bidi = run_cli_raw (
232- & server,
233- & [ "decode-invoice" , output_bidi[ "invoice" ] . as_str ( ) . unwrap ( ) ] ,
234- ) ;
257+ let raw_bidi =
258+ run_cli_raw ( & server, & [ "decode-invoice" , output_bidi[ "invoice" ] . as_str ( ) . unwrap ( ) ] ) ;
235259 assert ! (
236260 !raw_bidi. contains( '\u{202E}' ) ,
237261 "Raw CLI output must not contain bidi override characters"
238262 ) ;
239- assert ! (
240- raw_bidi. contains( "\\ u202E" ) ,
241- "Bidi characters should be escaped as \\ uXXXX in output"
242- ) ;
263+ assert ! ( raw_bidi. contains( "\\ u202E" ) , "Bidi characters should be escaped as \\ uXXXX in output" ) ;
243264}
244265
245266#[ tokio:: test]
@@ -307,30 +328,20 @@ async fn test_cli_decode_offer() {
307328 // Test that ANSI escape sequences cannot reach the terminal via CLI output.
308329 let desc_with_ansi = "offer\x1b [31m RED \x1b [0m" ;
309330 let output_ansi = run_cli ( & server_a, & [ "bolt12-receive" , desc_with_ansi] ) ;
310- let raw_decoded = run_cli_raw (
311- & server_a,
312- & [ "decode-offer" , output_ansi[ "offer" ] . as_str ( ) . unwrap ( ) ] ,
313- ) ;
314- assert ! (
315- !raw_decoded. contains( '\x1b' ) ,
316- "Raw CLI output must not contain ANSI escape bytes"
317- ) ;
331+ let raw_decoded =
332+ run_cli_raw ( & server_a, & [ "decode-offer" , output_ansi[ "offer" ] . as_str ( ) . unwrap ( ) ] ) ;
333+ assert ! ( !raw_decoded. contains( '\x1b' ) , "Raw CLI output must not contain ANSI escape bytes" ) ;
318334
319335 // Test that Unicode bidi override characters in the description are escaped
320336 let desc_with_bidi = "offer\u{202E} evil" ;
321337 let output_bidi = run_cli ( & server_a, & [ "bolt12-receive" , desc_with_bidi] ) ;
322- let raw_bidi = run_cli_raw (
323- & server_a,
324- & [ "decode-offer" , output_bidi[ "offer" ] . as_str ( ) . unwrap ( ) ] ,
325- ) ;
338+ let raw_bidi =
339+ run_cli_raw ( & server_a, & [ "decode-offer" , output_bidi[ "offer" ] . as_str ( ) . unwrap ( ) ] ) ;
326340 assert ! (
327341 !raw_bidi. contains( '\u{202E}' ) ,
328342 "Raw CLI output must not contain bidi override characters"
329343 ) ;
330- assert ! (
331- raw_bidi. contains( "\\ u202E" ) ,
332- "Bidi characters should be escaped as \\ uXXXX in output"
333- ) ;
344+ assert ! ( raw_bidi. contains( "\\ u202E" ) , "Bidi characters should be escaped as \\ uXXXX in output" ) ;
334345}
335346
336347#[ tokio:: test]
@@ -1148,3 +1159,44 @@ async fn test_metrics_endpoint_with_auth() {
11481159 assert ! ( metrics. contains( "ldk_server_total_anchor_channels_reserve_sats 0" ) ) ;
11491160 assert ! ( metrics. contains( "ldk_server_total_lightning_balance_sats 0" ) ) ;
11501161}
1162+
1163+ #[ tokio:: test]
1164+ async fn test_grpc_rejects_invalid_timeout_header ( ) {
1165+ let bitcoind = TestBitcoind :: new ( ) ;
1166+ let server = LdkServerHandle :: start ( & bitcoind) . await ;
1167+
1168+ let mut headers = HeaderMap :: new ( ) ;
1169+ headers. insert ( "grpc-timeout" , "abc" . parse ( ) . unwrap ( ) ) ;
1170+
1171+ let response =
1172+ raw_grpc_request ( & server, & format ! ( "/api.LightningNode/{GET_NODE_INFO_PATH}" ) , headers)
1173+ . await ;
1174+ assert_eq ! ( response. status( ) , 200 ) ;
1175+ assert_eq ! ( response. headers( ) . get( "grpc-status" ) . unwrap( ) , "3" ) ;
1176+ assert_eq ! ( response. headers( ) . get( "grpc-message" ) . unwrap( ) , "Invalid grpc-timeout header" ) ;
1177+ }
1178+
1179+ #[ tokio:: test]
1180+ async fn test_grpc_rejects_unknown_paths_and_methods ( ) {
1181+ let bitcoind = TestBitcoind :: new ( ) ;
1182+ let server = LdkServerHandle :: start ( & bitcoind) . await ;
1183+
1184+ let response = raw_grpc_request (
1185+ & server,
1186+ & format ! ( "/not-the-service/{GET_NODE_INFO_PATH}" ) ,
1187+ HeaderMap :: new ( ) ,
1188+ )
1189+ . await ;
1190+ assert_eq ! ( response. status( ) , 200 ) ;
1191+ assert_eq ! ( response. headers( ) . get( "grpc-status" ) . unwrap( ) , "12" ) ;
1192+ assert_eq ! (
1193+ response. headers( ) . get( "grpc-message" ) . unwrap( ) ,
1194+ "Unknown path%3A %2Fnot-the-service%2FGetNodeInfo"
1195+ ) ;
1196+
1197+ let response =
1198+ raw_grpc_request ( & server, "/api.LightningNode/DoesNotExist" , HeaderMap :: new ( ) ) . await ;
1199+ assert_eq ! ( response. status( ) , 200 ) ;
1200+ assert_eq ! ( response. headers( ) . get( "grpc-status" ) . unwrap( ) , "12" ) ;
1201+ assert_eq ! ( response. headers( ) . get( "grpc-message" ) . unwrap( ) , "Unknown method%3A DoesNotExist" ) ;
1202+ }
0 commit comments