1use std::time::Duration;
3
4use http::header::ACCEPT;
5use reqwest::{Client, Proxy};
6
7use crate::into_url::IntoUrl;
8use crate::OhttpKeys;
9
10pub async fn fetch_ohttp_keys(
19 ohttp_relay: impl IntoUrl,
20 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
32}
33
34#[cfg(feature = "_manual-tls")]
45pub async fn fetch_ohttp_keys_with_cert(
46 ohttp_relay: impl IntoUrl,
47 payjoin_directory: impl IntoUrl,
48 cert_der: &[u8],
49) -> 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()?;
58 let res = client
59 .get(ohttp_keys_url.as_str())
60 .timeout(Duration::from_secs(10))
61 .header(ACCEPT, "application/ohttp-keys")
62 .send()
63 .await?;
64 parse_ohttp_keys_response(res).await
65}
66
67async fn parse_ohttp_keys_response(res: reqwest::Response) -> Result<OhttpKeys, Error> {
68 if !res.status().is_success() {
69 return Err(Error::UnexpectedStatusCode(res.status()));
70 }
71
72 let body = res.bytes().await?.to_vec();
73 OhttpKeys::decode(&body).map_err(|e| {
74 Error::Internal(InternalError(InternalErrorInner::InvalidOhttpKeys(e.to_string())))
75 })
76}
77
78#[derive(Debug)]
79#[non_exhaustive]
80pub enum Error {
81 UnexpectedStatusCode(http::StatusCode),
83 #[doc(hidden)]
85 Internal(InternalError),
86}
87
88#[derive(Debug)]
89pub struct InternalError(InternalErrorInner);
90
91#[derive(Debug)]
92enum InternalErrorInner {
93 ParseUrl(crate::into_url::Error),
94 Reqwest(reqwest::Error),
95 Io(std::io::Error),
96 #[cfg(feature = "_manual-tls")]
97 Rustls(rustls::Error),
98 InvalidOhttpKeys(String),
99}
100
101impl From<crate::core::UrlParseError> for Error {
102 fn from(value: crate::core::UrlParseError) -> Self {
103 Self::Internal(InternalError(InternalErrorInner::ParseUrl(value.into())))
104 }
105}
106
107macro_rules! impl_from_error {
108 ($from:ty, $to:ident) => {
109 impl From<$from> for Error {
110 fn from(value: $from) -> Self {
111 Self::Internal(InternalError(InternalErrorInner::$to(value)))
112 }
113 }
114 };
115}
116
117impl_from_error!(crate::into_url::Error, ParseUrl);
118impl_from_error!(reqwest::Error, Reqwest);
119impl_from_error!(std::io::Error, Io);
120#[cfg(feature = "_manual-tls")]
121impl_from_error!(rustls::Error, Rustls);
122
123impl std::fmt::Display for Error {
124 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
125 match self {
126 Self::UnexpectedStatusCode(code) => {
127 write!(f, "Unexpected status code from payjoin directory: {code}")
128 }
129 Self::Internal(InternalError(e)) => e.fmt(f),
130 }
131 }
132}
133
134impl std::fmt::Display for InternalErrorInner {
135 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
136 use InternalErrorInner::*;
137
138 match &self {
139 Reqwest(e) => e.fmt(f),
140 ParseUrl(e) => e.fmt(f),
141 Io(e) => e.fmt(f),
142 InvalidOhttpKeys(e) => {
143 write!(f, "Invalid ohttp keys returned from payjoin directory: {e}")
144 }
145 #[cfg(feature = "_manual-tls")]
146 Rustls(e) => e.fmt(f),
147 }
148 }
149}
150
151impl std::error::Error for Error {
152 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
153 match self {
154 Self::Internal(InternalError(e)) => e.source(),
155 Self::UnexpectedStatusCode(_) => None,
156 }
157 }
158}
159
160impl std::error::Error for InternalErrorInner {
161 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
162 use InternalErrorInner::*;
163
164 match self {
165 Reqwest(e) => Some(e),
166 ParseUrl(e) => Some(e),
167 Io(e) => Some(e),
168 InvalidOhttpKeys(_) => None,
169 #[cfg(feature = "_manual-tls")]
170 Rustls(e) => Some(e),
171 }
172 }
173}
174
175impl From<InternalError> for Error {
176 fn from(value: InternalError) -> Self { Self::Internal(value) }
177}
178
179impl From<InternalErrorInner> for Error {
180 fn from(value: InternalErrorInner) -> Self { Self::Internal(InternalError(value)) }
181}
182
183#[cfg(test)]
184mod tests {
185 use http::StatusCode;
186 use reqwest::Response;
187
188 use super::*;
189
190 fn mock_response(status: StatusCode, body: Vec<u8>) -> Response {
191 Response::from(http::response::Response::builder().status(status).body(body).unwrap())
192 }
193
194 #[tokio::test]
195 async fn test_parse_success_response() {
196 let valid_keys = payjoin_test_utils::ohttp_key_config_bytes();
197
198 let response = mock_response(StatusCode::OK, valid_keys);
199 assert!(parse_ohttp_keys_response(response).await.is_ok(), "expected valid keys response");
200 }
201
202 #[tokio::test]
203 async fn test_parse_error_status_codes() {
204 let error_codes = [
205 StatusCode::BAD_REQUEST,
206 StatusCode::NOT_FOUND,
207 StatusCode::INTERNAL_SERVER_ERROR,
208 StatusCode::SERVICE_UNAVAILABLE,
209 ];
210
211 for status in error_codes {
212 let response = mock_response(status, vec![]);
213 match parse_ohttp_keys_response(response).await {
214 Err(Error::UnexpectedStatusCode(code)) => assert_eq!(code, status),
215 result => panic!(
216 "Expected UnexpectedStatusCode error for status code: {status}, got: {result:?}"
217 ),
218 }
219 }
220 }
221
222 #[tokio::test]
223 async fn test_parse_invalid_keys() {
224 let invalid_keys = vec![1, 2, 3, 4];
226
227 let response = mock_response(StatusCode::OK, invalid_keys);
228
229 assert!(
230 matches!(
231 parse_ohttp_keys_response(response).await,
232 Err(Error::Internal(InternalError(InternalErrorInner::InvalidOhttpKeys(_))))
233 ),
234 "expected InvalidOhttpKeys error"
235 );
236 }
237}