Skip to content

Commit d8c37c0

Browse files
authored
Move shared response processing code to ohttp.rs (payjoin#660)
The `process_res` methods across send and receive have a lot of shared code that can be extracted into reusable functions. `process_get_res` and `process_post_res` essentially do the same thing, and could conceivably be combined into one function. They were kept separate because "202 Accepted" is not a valid response for POST requests.
2 parents 1063687 + ec4b67e commit d8c37c0

File tree

5 files changed

+112
-92
lines changed

5 files changed

+112
-92
lines changed

payjoin/src/ohttp.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,77 @@ pub fn ohttp_encapsulate(
5252
Ok((buffer, ohttp_ctx))
5353
}
5454

55+
#[derive(Debug)]
56+
pub enum DirectoryResponseError {
57+
InvalidSize(usize),
58+
OhttpDecapsulation(OhttpEncapsulationError),
59+
UnexpectedStatusCode(http::StatusCode),
60+
}
61+
62+
impl fmt::Display for DirectoryResponseError {
63+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
64+
use DirectoryResponseError::*;
65+
66+
match self {
67+
OhttpDecapsulation(e) => write!(f, "OHTTP Decapsulation Error: {e}"),
68+
InvalidSize(size) => write!(
69+
f,
70+
"Unexpected response size {}, expected {} bytes",
71+
size,
72+
crate::directory::ENCAPSULATED_MESSAGE_BYTES
73+
),
74+
UnexpectedStatusCode(status) => write!(f, "Unexpected status code: {status}"),
75+
}
76+
}
77+
}
78+
79+
impl error::Error for DirectoryResponseError {
80+
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
81+
use DirectoryResponseError::*;
82+
83+
match self {
84+
OhttpDecapsulation(e) => Some(e),
85+
InvalidSize(_) => None,
86+
UnexpectedStatusCode(_) => None,
87+
}
88+
}
89+
}
90+
91+
pub fn process_get_res(
92+
res: &[u8],
93+
ohttp_context: ohttp::ClientResponse,
94+
) -> Result<Option<Vec<u8>>, DirectoryResponseError> {
95+
let response = process_ohttp_res(res, ohttp_context)?;
96+
match response.status() {
97+
http::StatusCode::OK => Ok(Some(response.body().to_vec())),
98+
http::StatusCode::ACCEPTED => Ok(None),
99+
status_code => Err(DirectoryResponseError::UnexpectedStatusCode(status_code)),
100+
}
101+
}
102+
103+
pub fn process_post_res(
104+
res: &[u8],
105+
ohttp_context: ohttp::ClientResponse,
106+
) -> Result<(), DirectoryResponseError> {
107+
let response = process_ohttp_res(res, ohttp_context)?;
108+
match response.status() {
109+
http::StatusCode::OK => Ok(()),
110+
status_code => Err(DirectoryResponseError::UnexpectedStatusCode(status_code)),
111+
}
112+
}
113+
114+
fn process_ohttp_res(
115+
res: &[u8],
116+
ohttp_context: ohttp::ClientResponse,
117+
) -> Result<http::Response<Vec<u8>>, DirectoryResponseError> {
118+
let response_array: &[u8; crate::directory::ENCAPSULATED_MESSAGE_BYTES] =
119+
res.try_into().map_err(|_| DirectoryResponseError::InvalidSize(res.len()))?;
120+
log::trace!("decapsulating directory response");
121+
let res = ohttp_decapsulate(ohttp_context, response_array)
122+
.map_err(DirectoryResponseError::OhttpDecapsulation)?;
123+
Ok(res)
124+
}
125+
55126
/// decapsulate ohttp, bhttp response and return http response body and status code
56127
pub fn ohttp_decapsulate(
57128
res_ctx: ohttp::ClientResponse,

payjoin/src/receive/v2/error.rs

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::error;
33

44
use super::Error::V2;
55
use crate::hpke::HpkeError;
6-
use crate::ohttp::OhttpEncapsulationError;
6+
use crate::ohttp::{DirectoryResponseError, OhttpEncapsulationError};
77
use crate::receive::error::Error;
88

99
/// Error that may occur during a v2 session typestate change
@@ -31,10 +31,8 @@ pub(crate) enum InternalSessionError {
3131
OhttpEncapsulation(OhttpEncapsulationError),
3232
/// Hybrid Public Key Encryption failed
3333
Hpke(HpkeError),
34-
/// Unexpected response size
35-
UnexpectedResponseSize(usize),
36-
/// Unexpected status code
37-
UnexpectedStatusCode(http::StatusCode),
34+
/// The directory returned a bad response
35+
DirectoryResponse(DirectoryResponseError),
3836
}
3937

4038
impl From<OhttpEncapsulationError> for Error {
@@ -56,13 +54,7 @@ impl fmt::Display for SessionError {
5654
Expired(expiry) => write!(f, "Session expired at {expiry:?}"),
5755
OhttpEncapsulation(e) => write!(f, "OHTTP Encapsulation Error: {e}"),
5856
Hpke(e) => write!(f, "Hpke decryption failed: {e}"),
59-
UnexpectedResponseSize(size) => write!(
60-
f,
61-
"Unexpected response size {}, expected {} bytes",
62-
size,
63-
crate::directory::ENCAPSULATED_MESSAGE_BYTES
64-
),
65-
UnexpectedStatusCode(status) => write!(f, "Unexpected status code: {status}"),
57+
DirectoryResponse(e) => write!(f, "Directory response error: {e}"),
6658
}
6759
}
6860
}
@@ -76,8 +68,7 @@ impl error::Error for SessionError {
7668
Expired(_) => None,
7769
OhttpEncapsulation(e) => Some(e),
7870
Hpke(e) => Some(e),
79-
UnexpectedResponseSize(_) => None,
80-
UnexpectedStatusCode(_) => None,
71+
DirectoryResponse(e) => Some(e),
8172
}
8273
}
8374
}

payjoin/src/receive/v2/mod.rs

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ use super::{
1717
v1, InternalPayloadError, JsonReply, OutputSubstitutionError, ReplyableError, SelectionError,
1818
};
1919
use crate::hpke::{decrypt_message_a, encrypt_message_b, HpkeKeyPair, HpkePublicKey};
20-
use crate::ohttp::{ohttp_decapsulate, ohttp_encapsulate, OhttpEncapsulationError, OhttpKeys};
20+
use crate::ohttp::{
21+
ohttp_encapsulate, process_get_res, process_post_res, OhttpEncapsulationError, OhttpKeys,
22+
};
2123
use crate::output_substitution::OutputSubstitution;
2224
use crate::persist::Persister;
2325
use crate::receive::{parse_payload, InputPair};
@@ -180,23 +182,17 @@ impl Receiver<WithContext> {
180182
body: &[u8],
181183
context: ohttp::ClientResponse,
182184
) -> Result<Option<Receiver<UncheckedProposal>>, Error> {
183-
let response_array: &[u8; crate::directory::ENCAPSULATED_MESSAGE_BYTES] =
184-
body.try_into()
185-
.map_err(|_| InternalSessionError::UnexpectedResponseSize(body.len()))?;
186-
log::trace!("decapsulating directory response");
187-
let response = ohttp_decapsulate(context, response_array)
188-
.map_err(InternalSessionError::OhttpEncapsulation)?;
189-
if response.body().is_empty() {
190-
log::debug!("response is empty");
191-
return Ok(None);
192-
}
193-
match String::from_utf8(response.body().to_vec()) {
185+
let body = match process_get_res(body, context)
186+
.map_err(InternalSessionError::DirectoryResponse)?
187+
{
188+
Some(body) => body,
189+
None => return Ok(None),
190+
};
191+
match String::from_utf8(body.clone()) {
194192
// V1 response bodies are utf8 plaintext
195193
Ok(response) => Ok(Some(Receiver { state: self.extract_proposal_from_v1(response)? })),
196194
// V2 response bodies are encrypted binary
197-
Err(_) => Ok(Some(Receiver {
198-
state: self.extract_proposal_from_v2(response.body().to_vec())?,
199-
})),
195+
Err(_) => Ok(Some(Receiver { state: self.extract_proposal_from_v2(body)? })),
200196
}
201197
}
202198

@@ -350,16 +346,8 @@ impl Receiver<UncheckedProposal> {
350346
body: &[u8],
351347
context: ohttp::ClientResponse,
352348
) -> Result<(), SessionError> {
353-
let response_array: &[u8; crate::directory::ENCAPSULATED_MESSAGE_BYTES] =
354-
body.try_into()
355-
.map_err(|_| InternalSessionError::UnexpectedResponseSize(body.len()))?;
356-
let response = ohttp_decapsulate(context, response_array)
357-
.map_err(InternalSessionError::OhttpEncapsulation)?;
358-
359-
match response.status() {
360-
http::StatusCode::OK => Ok(()),
361-
_ => Err(InternalSessionError::UnexpectedStatusCode(response.status()).into()),
362-
}
349+
process_post_res(body, context)
350+
.map_err(|e| InternalSessionError::DirectoryResponse(e).into())
363351
}
364352
}
365353

@@ -648,15 +636,8 @@ impl Receiver<PayjoinProposal> {
648636
res: &[u8],
649637
ohttp_context: ohttp::ClientResponse,
650638
) -> Result<(), Error> {
651-
let response_array: &[u8; crate::directory::ENCAPSULATED_MESSAGE_BYTES] =
652-
res.try_into().map_err(|_| InternalSessionError::UnexpectedResponseSize(res.len()))?;
653-
let res = ohttp_decapsulate(ohttp_context, response_array)
654-
.map_err(InternalSessionError::OhttpEncapsulation)?;
655-
if res.status().is_success() {
656-
Ok(())
657-
} else {
658-
Err(InternalSessionError::UnexpectedStatusCode(res.status()).into())
659-
}
639+
process_post_res(res, ohttp_context)
640+
.map_err(|e| InternalSessionError::DirectoryResponse(e).into())
660641
}
661642
}
662643

payjoin/src/send/v2/error.rs

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use core::fmt;
22

3+
use crate::ohttp::DirectoryResponseError;
34
use crate::uri::url_ext::ParseReceiverPubkeyParamError;
45

56
/// Error returned when request could not be created.
@@ -73,25 +74,19 @@ pub struct EncapsulationError(InternalEncapsulationError);
7374

7475
#[derive(Debug)]
7576
pub(crate) enum InternalEncapsulationError {
76-
/// The response size is not the expected size.
77-
InvalidSize(usize),
78-
/// The status code is not the expected status code.
79-
UnexpectedStatusCode(http::StatusCode),
8077
/// The HPKE failed.
8178
Hpke(crate::hpke::HpkeError),
82-
/// The encapsulation failed.
83-
Ohttp(crate::ohttp::OhttpEncapsulationError),
79+
/// The directory returned a bad response
80+
DirectoryResponse(DirectoryResponseError),
8481
}
8582

8683
impl fmt::Display for EncapsulationError {
8784
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
8885
use InternalEncapsulationError::*;
8986

9087
match &self.0 {
91-
InvalidSize(size) => write!(f, "invalid size: {size}"),
92-
UnexpectedStatusCode(status) => write!(f, "unexpected status code: {status}"),
93-
Ohttp(error) => write!(f, "OHTTP encapsulation error: {error}"),
9488
Hpke(error) => write!(f, "HPKE error: {error}"),
89+
DirectoryResponse(e) => write!(f, "Directory response error: {e}"),
9590
}
9691
}
9792
}
@@ -101,10 +96,8 @@ impl std::error::Error for EncapsulationError {
10196
use InternalEncapsulationError::*;
10297

10398
match &self.0 {
104-
InvalidSize(_) => None,
105-
UnexpectedStatusCode(_) => None,
106-
Ohttp(error) => Some(error),
10799
Hpke(error) => Some(error),
100+
DirectoryResponse(e) => Some(e),
108101
}
109102
}
110103
}
@@ -115,8 +108,6 @@ impl From<InternalEncapsulationError> for EncapsulationError {
115108

116109
impl From<InternalEncapsulationError> for super::ResponseError {
117110
fn from(value: InternalEncapsulationError) -> Self {
118-
super::ResponseError::Validation(
119-
super::InternalValidationError::V2Encapsulation(value.into()).into(),
120-
)
111+
super::InternalValidationError::V2Encapsulation(value.into()).into()
121112
}
122113
}

payjoin/src/send/v2/mod.rs

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use url::Url;
3232
use super::error::BuildSenderError;
3333
use super::*;
3434
use crate::hpke::{decrypt_message_b, encrypt_message_a, HpkeSecretKey};
35-
use crate::ohttp::{ohttp_decapsulate, ohttp_encapsulate};
35+
use crate::ohttp::{ohttp_encapsulate, process_get_res, process_post_res};
3636
use crate::persist::Persister;
3737
use crate::send::v1;
3838
use crate::uri::{ShortId, UrlExt};
@@ -325,24 +325,15 @@ impl Sender<V2PostContext> {
325325
self,
326326
response: &[u8],
327327
) -> Result<Sender<V2GetContext>, EncapsulationError> {
328-
let response_array: &[u8; crate::directory::ENCAPSULATED_MESSAGE_BYTES] = response
329-
.try_into()
330-
.map_err(|_| InternalEncapsulationError::InvalidSize(response.len()))?;
331-
let response = ohttp_decapsulate(self.state.ohttp_ctx, response_array)
332-
.map_err(InternalEncapsulationError::Ohttp)?;
333-
match response.status() {
334-
http::StatusCode::OK => {
335-
// return OK with new Typestate
336-
Ok(Sender {
337-
state: V2GetContext {
338-
endpoint: self.state.endpoint,
339-
psbt_ctx: self.state.psbt_ctx,
340-
hpke_ctx: self.state.hpke_ctx,
341-
},
342-
})
343-
}
344-
_ => Err(InternalEncapsulationError::UnexpectedStatusCode(response.status()))?,
345-
}
328+
process_post_res(response, self.state.ohttp_ctx)
329+
.map_err(InternalEncapsulationError::DirectoryResponse)?;
330+
Ok(Sender {
331+
state: V2GetContext {
332+
endpoint: self.state.endpoint,
333+
psbt_ctx: self.state.psbt_ctx,
334+
hpke_ctx: self.state.hpke_ctx,
335+
},
336+
})
346337
}
347338
}
348339

@@ -404,16 +395,11 @@ impl Sender<V2GetContext> {
404395
response: &[u8],
405396
ohttp_ctx: ohttp::ClientResponse,
406397
) -> Result<Option<Psbt>, ResponseError> {
407-
let response_array: &[u8; crate::directory::ENCAPSULATED_MESSAGE_BYTES] = response
408-
.try_into()
409-
.map_err(|_| InternalEncapsulationError::InvalidSize(response.len()))?;
410-
411-
let response = ohttp_decapsulate(ohttp_ctx, response_array)
412-
.map_err(InternalEncapsulationError::Ohttp)?;
413-
let body = match response.status() {
414-
http::StatusCode::OK => response.body().to_vec(),
415-
http::StatusCode::ACCEPTED => return Ok(None),
416-
_ => return Err(InternalEncapsulationError::UnexpectedStatusCode(response.status()))?,
398+
let body = match process_get_res(response, ohttp_ctx)
399+
.map_err(InternalEncapsulationError::DirectoryResponse)?
400+
{
401+
Some(body) => body,
402+
None => return Ok(None),
417403
};
418404
let psbt = decrypt_message_b(
419405
&body,

0 commit comments

Comments
 (0)