@@ -30,11 +30,12 @@ use tower_ohttp::test_utils::{
3030} ;
3131use tower_ohttp:: OhttpLayer ;
3232
33+ use crate :: server:: request_log:: { RequestLogLayer , REQUEST_ID_HEADER } ;
34+ use crate :: server:: request_span:: RequestSpanLayer ;
35+
3336const DEFAULT_BODY_LIMIT : usize = 102_400 ;
3437const KEY_CACHE_SECS : u64 = 3600 ;
3538
36- /// Body builder for jsonrpsee's `HttpBody`. Returned as a `fn` pointer to
37- /// give `OhttpLayer` a sized, `Copy` closure type without an `as` cast.
3839fn body_builder ( ) -> fn ( Full < bytes:: Bytes > ) -> HttpBody {
3940 HttpBody :: new
4041}
@@ -95,12 +96,15 @@ async fn production_chain_compresses_inner_not_outer() {
9596 let ohttp_layer =
9697 OhttpLayer :: new ( gateway. clone ( ) , DEFAULT_BODY_LIMIT , KEY_CACHE_SECS , body_builder ( ) ) ;
9798
98- // Replicates the production ServiceBuilder chain from `server.rs`/`tls.rs`.
99- // Must be kept in sync with those files.
99+ // Replicates the OHTTP body-handling portion of the production chain in
100+ // `server.rs`/`tls.rs` (the outermost observability layers — request log,
101+ // health, metrics — don't affect body/compression handling and are
102+ // omitted). `RequestSpanLayer` is included since it sits inside OHTTP.
100103 let mut svc = tower:: ServiceBuilder :: new ( )
101104 . option_layer ( None :: < CorsLayer > )
102105 . layer ( MapRequestBodyLayer :: new ( HttpBody :: new) )
103106 . option_layer ( Some ( ohttp_layer) )
107+ . layer ( RequestSpanLayer )
104108 . layer ( MapResponseBodyLayer :: new ( HttpBody :: new) )
105109 . layer ( CompressionLayer :: new ( ) )
106110 . service ( tower:: service_fn ( jsonrpsee_echo_service) ) ;
@@ -176,3 +180,75 @@ async fn non_ohttp_request_passes_through_jsonrpsee() {
176180 let body = response. into_body ( ) . collect ( ) . await . unwrap ( ) . to_bytes ( ) ;
177181 assert_eq ! ( body. as_ref( ) , json_body) ;
178182}
183+
184+ /// End-to-end OHTTP unlinkability: the request-id echoed on the OUTER
185+ /// (relay-visible) response must differ from the fresh id bound to the
186+ /// decapsulated inner dispatch, and the client-supplied inner id must be
187+ /// discarded — so no shared key links the relay's view to the gateway's.
188+ /// Exercises the real decapsulation path through
189+ /// `RequestLogLayer → OhttpLayer → RequestSpanLayer`.
190+ #[ tokio:: test]
191+ async fn ohttp_inner_request_id_unlinkable_from_envelope ( ) {
192+ let gateway = test_gateway ( ) ;
193+ let ohttp_layer =
194+ OhttpLayer :: new ( gateway. clone ( ) , DEFAULT_BODY_LIMIT , KEY_CACHE_SECS , body_builder ( ) ) ;
195+
196+ // Inner service echoes the request-id it observes into the response body.
197+ let echo_id = tower:: service_fn ( |req : http:: Request < HttpBody > | async move {
198+ let id = req. headers ( ) . get ( REQUEST_ID_HEADER ) . map ( |v| v. to_str ( ) . unwrap ( ) ) . unwrap_or ( "" ) ;
199+ Ok :: < _ , BoxError > (
200+ http:: Response :: builder ( )
201+ . status ( http:: StatusCode :: OK )
202+ . body ( HttpBody :: from ( id. as_bytes ( ) . to_vec ( ) ) )
203+ . unwrap ( ) ,
204+ )
205+ } ) ;
206+
207+ let mut svc = tower:: ServiceBuilder :: new ( )
208+ . layer ( RequestLogLayer )
209+ . layer ( MapRequestBodyLayer :: new ( HttpBody :: new) )
210+ . option_layer ( Some ( ohttp_layer) )
211+ . layer ( RequestSpanLayer )
212+ . layer ( MapResponseBodyLayer :: new ( HttpBody :: new) )
213+ . service ( echo_id) ;
214+
215+ // The envelope carries a client-chosen inner id that must be discarded.
216+ let ( encapsulated, client_response) = encapsulate_bhttp_request (
217+ & gateway,
218+ "POST" ,
219+ "/" ,
220+ b"" ,
221+ & [ ( "x-request-id" , b"inner-client-id" ) ] ,
222+ ) ;
223+
224+ // The outer envelope request carries the relay-visible id.
225+ let mut outer = ohttp_http_request ( encapsulated) ;
226+ outer
227+ . headers_mut ( )
228+ . insert ( REQUEST_ID_HEADER , http:: HeaderValue :: from_static ( "envelope-relay-id" ) ) ;
229+
230+ let response = svc. call ( outer) . await . unwrap ( ) ;
231+
232+ // The outer (relay-visible) response echoes the envelope id.
233+ let envelope_id =
234+ response. headers ( ) . get ( REQUEST_ID_HEADER ) . unwrap ( ) . to_str ( ) . unwrap ( ) . to_owned ( ) ;
235+ assert_eq ! ( envelope_id, "envelope-relay-id" ) ;
236+
237+ let encrypted_body = response. into_body ( ) . collect ( ) . await . unwrap ( ) . to_bytes ( ) ;
238+ let decapsulated = decapsulate_bhttp_response ( client_response, & encrypted_body) ;
239+ assert_eq ! ( decapsulated. status, 200 ) ;
240+ let inner_id = String :: from_utf8 ( decapsulated. body ) . expect ( "utf8 inner id" ) ;
241+
242+ assert_ne ! ( inner_id, envelope_id, "inner id must not equal the relay-visible envelope id" ) ;
243+ assert_ne ! ( inner_id, "inner-client-id" , "client-supplied inner id must be discarded" ) ;
244+ assert ! (
245+ uuid:: Uuid :: parse_str( & inner_id) . is_ok( ) ,
246+ "inner id must be a fresh UUID, got {inner_id:?}"
247+ ) ;
248+ // No id is set on the inner *response*, so nothing — neither the envelope
249+ // id nor the fresh content id — leaks into the encrypted reply's headers.
250+ assert ! (
251+ decapsulated. bhttp_message. header( ) . get( b"x-request-id" ) . is_none( ) ,
252+ "inner OHTTP response must not carry an x-request-id header"
253+ ) ;
254+ }
0 commit comments