11//! IO-related types and functions. Specifically, fetching OHTTP keys from a payjoin directory.
22use std:: time:: Duration ;
33
4+ use bitcoin:: secp256k1:: rand;
45use http:: header:: ACCEPT ;
56use reqwest:: { Client , Proxy } ;
67
78use crate :: into_url:: IntoUrl ;
8- use crate :: OhttpKeys ;
9+ use crate :: relay:: RelaySelector ;
10+ use crate :: { OhttpKeys , Url } ;
911
1012/// Fetch the ohttp keys from the specified payjoin directory via proxy.
1113///
12- /// * `ohttp_relay `: The http CONNECT method proxy to request the ohttp keys from a payjoin
13- /// directory. Proxying requests for ohttp keys ensures a client IP address is never revealed to
14- /// the payjoin directory.
14+ /// * `relays `: The http CONNECT method proxies to request the ohttp keys from a payjoin
15+ /// directory, tried in random order until one succeeds. Proxying requests for ohttp keys
16+ /// ensures a client IP address is never revealed to the payjoin directory.
1517///
1618/// * `payjoin_directory`: The payjoin directory from which to fetch the ohttp keys. This
1719/// directory stores and forwards payjoin client payloads.
20+ ///
21+ /// Returns the ohttp keys and the relay that served them.
1822pub async fn fetch_ohttp_keys (
19- ohttp_relay : impl IntoUrl ,
23+ relays : & [ Url ] ,
2024 payjoin_directory : impl IntoUrl ,
21- ) -> Result < OhttpKeys , Error > {
22- let ohttp_keys_url = payjoin_directory. into_url ( ) ?. join ( "/.well-known/ohttp-gateway" ) ?;
23- let proxy = Proxy :: all ( ohttp_relay. into_url ( ) ?. as_str ( ) ) ?;
24- let client = Client :: builder ( ) . proxy ( proxy) . http1_only ( ) . build ( ) ?;
25- let res = client
26- . get ( ohttp_keys_url. as_str ( ) )
27- . timeout ( Duration :: from_secs ( 10 ) )
28- . header ( ACCEPT , "application/ohttp-keys" )
29- . send ( )
30- . await ?;
31- parse_ohttp_keys_response ( res) . await
25+ ) -> Result < ( OhttpKeys , Url ) , Error > {
26+ fetch_ohttp_keys_inner ( relays, payjoin_directory. into_url ( ) ?, None ) . await
3227}
3328
3429/// Fetch the ohttp keys from the specified payjoin directory via proxy.
3530///
36- /// * `ohttp_relay `: The http CONNECT method proxy to request the ohttp keys from a payjoin
37- /// directory. Proxying requests for ohttp keys ensures a client IP address is never revealed to
38- /// the payjoin directory.
31+ /// * `relays `: The http CONNECT method proxies to request the ohttp keys from a payjoin
32+ /// directory, tried in random order until one succeeds. Proxying requests for ohttp keys
33+ /// ensures a client IP address is never revealed to the payjoin directory.
3934///
4035/// * `payjoin_directory`: The payjoin directory from which to fetch the ohttp keys. This
4136/// directory stores and forwards payjoin client payloads.
4237///
4338/// * `cert_der`: The DER-encoded certificate to use for local HTTPS connections.
39+ ///
40+ /// Returns the ohttp keys and the relay that served them.
4441#[ cfg( feature = "_manual-tls" ) ]
4542pub async fn fetch_ohttp_keys_with_cert (
46- ohttp_relay : impl IntoUrl ,
43+ relays : & [ Url ] ,
4744 payjoin_directory : impl IntoUrl ,
4845 cert_der : & [ u8 ] ,
46+ ) -> Result < ( OhttpKeys , Url ) , Error > {
47+ fetch_ohttp_keys_inner ( relays, payjoin_directory. into_url ( ) ?, Some ( cert_der) ) . await
48+ }
49+
50+ async fn fetch_ohttp_keys_inner (
51+ relays : & [ Url ] ,
52+ payjoin_directory : Url ,
53+ cert_der : Option < & [ u8 ] > ,
54+ ) -> Result < ( OhttpKeys , Url ) , Error > {
55+ let ohttp_keys_url = payjoin_directory. join ( "/.well-known/ohttp-gateway" ) ?;
56+ let mut selector = RelaySelector :: new ( relays. to_vec ( ) ) ;
57+ let mut last_err: Option < Error > = None ;
58+ loop {
59+ let relay = match selector. select ( & mut rand:: thread_rng ( ) ) {
60+ Some ( relay) => relay,
61+ None => return Err ( last_err. unwrap_or ( Error :: NoRelaysAvailable ) ) ,
62+ } ;
63+ match fetch_keys_once ( & relay, & ohttp_keys_url, cert_der) . await {
64+ Ok ( keys) => return Ok ( ( keys, relay) ) ,
65+ Err ( e @ Error :: UnexpectedStatusCode ( _) ) => return Err ( e) ,
66+ Err ( e) => {
67+ selector. mark_failed ( & relay) ;
68+ last_err = Some ( e) ;
69+ }
70+ }
71+ }
72+ }
73+
74+ async fn fetch_keys_once (
75+ relay : & Url ,
76+ ohttp_keys_url : & Url ,
77+ cert_der : Option < & [ u8 ] > ,
4978) -> Result < OhttpKeys , Error > {
50- let ohttp_keys_url = payjoin_directory. into_url ( ) ?. join ( "/.well-known/ohttp-gateway" ) ?;
51- let proxy = Proxy :: all ( ohttp_relay. into_url ( ) ?. as_str ( ) ) ?;
52- let client = Client :: builder ( )
53- . use_rustls_tls ( )
54- . add_root_certificate ( reqwest:: tls:: Certificate :: from_der ( cert_der) ?)
55- . proxy ( proxy)
56- . http1_only ( )
57- . build ( ) ?;
79+ #[ cfg( not( feature = "_manual-tls" ) ) ]
80+ let _ = cert_der;
81+ let proxy = Proxy :: all ( relay. as_str ( ) ) ?;
82+ let builder = Client :: builder ( ) . proxy ( proxy) . http1_only ( ) ;
83+ #[ cfg( feature = "_manual-tls" ) ]
84+ let builder = match cert_der {
85+ Some ( cert_der) => builder
86+ . use_rustls_tls ( )
87+ . add_root_certificate ( reqwest:: tls:: Certificate :: from_der ( cert_der) ?) ,
88+ None => builder,
89+ } ;
90+ let client = builder. build ( ) ?;
5891 let res = client
5992 . get ( ohttp_keys_url. as_str ( ) )
6093 . timeout ( Duration :: from_secs ( 10 ) )
@@ -80,6 +113,8 @@ async fn parse_ohttp_keys_response(res: reqwest::Response) -> Result<OhttpKeys,
80113pub enum Error {
81114 /// When the payjoin directory returns an unexpected status code
82115 UnexpectedStatusCode ( http:: StatusCode ) ,
116+ /// No relay was available to reach the payjoin directory.
117+ NoRelaysAvailable ,
83118 /// Internal errors that should not be pattern matched by users
84119 #[ doc( hidden) ]
85120 Internal ( InternalError ) ,
@@ -126,6 +161,7 @@ impl std::fmt::Display for Error {
126161 Self :: UnexpectedStatusCode ( code) => {
127162 write ! ( f, "Unexpected status code from payjoin directory: {code}" )
128163 }
164+ Self :: NoRelaysAvailable => write ! ( f, "No relay was available to fetch ohttp keys" ) ,
129165 Self :: Internal ( InternalError ( e) ) => e. fmt ( f) ,
130166 }
131167 }
@@ -153,6 +189,7 @@ impl std::error::Error for Error {
153189 match self {
154190 Self :: Internal ( InternalError ( e) ) => e. source ( ) ,
155191 Self :: UnexpectedStatusCode ( _) => None ,
192+ Self :: NoRelaysAvailable => None ,
156193 }
157194 }
158195}
@@ -240,4 +277,27 @@ mod tests {
240277 "expected InvalidOhttpKeys error"
241278 ) ;
242279 }
280+
281+ #[ tokio:: test]
282+ async fn fetch_with_no_relays_returns_no_relays_available ( ) {
283+ assert ! ( matches!(
284+ fetch_ohttp_keys( & [ ] , "https://directory.example" ) . await ,
285+ Err ( Error :: NoRelaysAvailable )
286+ ) ) ;
287+ }
288+
289+ #[ tokio:: test]
290+ async fn fetch_exhausts_unreachable_relays_and_surfaces_last_error ( ) {
291+ // Every relay fails to connect: the loop marks each failed and terminates
292+ // once none remain, surfacing the last transport error. NoRelaysAvailable
293+ // is reserved for an empty relay list, where no attempt was ever made.
294+ let relays = [
295+ Url :: parse ( "http://127.0.0.1:1" ) . expect ( "valid url" ) ,
296+ Url :: parse ( "http://127.0.0.1:2" ) . expect ( "valid url" ) ,
297+ ] ;
298+ assert ! ( matches!(
299+ fetch_ohttp_keys( & relays, "https://directory.example" ) . await ,
300+ Err ( Error :: Internal ( _) )
301+ ) ) ;
302+ }
243303}
0 commit comments