Skip to content

Commit 8933188

Browse files
authored
Simplify v1 response processing function signatures (#745)
This was prompted by the discussion in #710 (comment). Using a reader still left callers susceptible to writing memory exhaustion vulnerable code, and just introduced extra complexity by having to buffer twice. We can recognize this and leave the buffering entirely up to the caller. Also, the V2 sender and receiver response processing functions already take &[u8], so this makes the function signatures more consistent across versions.
2 parents 4f04d45 + b508d01 commit 8933188

12 files changed

Lines changed: 46 additions & 80 deletions

File tree

payjoin-cli/src/app/v1.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use anyhow::{anyhow, Context, Result};
77
use bitcoincore_rpc::bitcoin::Amount;
88
use http_body_util::combinators::BoxBody;
99
use http_body_util::{BodyExt, Full};
10-
use hyper::body::{Buf, Bytes, Incoming};
10+
use hyper::body::{Bytes, Incoming};
1111
use hyper::server::conn::http1;
1212
use hyper::service::service_fn;
1313
use hyper::{Method, Request, Response, StatusCode};
@@ -88,12 +88,10 @@ impl AppTrait for App {
8888
"Sent fallback transaction hex: {:#}",
8989
payjoin::bitcoin::consensus::encode::serialize_hex(&fallback_tx)
9090
);
91-
let psbt = ctx.process_response(&mut response.bytes().await?.to_vec().as_slice()).map_err(
92-
|e| {
93-
log::debug!("Error processing response: {e:?}");
94-
anyhow!("Failed to process response {e}")
95-
},
96-
)?;
91+
let psbt = ctx.process_response(&response.bytes().await?).map_err(|e| {
92+
log::debug!("Error processing response: {e:?}");
93+
anyhow!("Failed to process response {e}")
94+
})?;
9795

9896
self.process_pj_response(psbt)?;
9997
Ok(())
@@ -279,8 +277,8 @@ impl App {
279277
let (parts, body) = req.into_parts();
280278
let headers = Headers(&parts.headers);
281279
let query_string = parts.uri.query().unwrap_or("");
282-
let body = body.collect().await.map_err(|e| Implementation(e.into()))?.aggregate().reader();
283-
let proposal = UncheckedProposal::from_request(body, query_string, headers)?;
280+
let body = body.collect().await.map_err(|e| Implementation(e.into()))?.to_bytes();
281+
let proposal = UncheckedProposal::from_request(&body, query_string, headers)?;
284282

285283
let payjoin_proposal = self.process_v1_proposal(proposal)?;
286284
let psbt = payjoin_proposal.psbt();

payjoin-cli/src/app/v2/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ impl App {
234234
println!("Posting Original PSBT Payload request...");
235235
let response = post_request(req).await?;
236236
println!("Sent fallback transaction");
237-
match v1_ctx.process_response(&mut response.bytes().await?.to_vec().as_slice()) {
237+
match v1_ctx.process_response(&response.bytes().await?) {
238238
Ok(psbt) => Ok(psbt),
239239
Err(re) => {
240240
println!("{re}");

payjoin-ffi/src/send/mod.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use std::io::Cursor;
21
use std::str::FromStr;
32
use std::sync::{Arc, Mutex};
43

@@ -185,10 +184,9 @@ impl From<payjoin::send::v1::V1Context> for V1Context {
185184
impl V1Context {
186185
///Decodes and validates the response.
187186
/// Call this method with response from receiver to continue BIP78 flow. If the response is valid you will get appropriate PSBT that you should sign and broadcast.
188-
pub fn process_response(&self, response: Vec<u8>) -> Result<String, ResponseError> {
189-
let mut decoder = Cursor::new(response);
187+
pub fn process_response(&self, response: &[u8]) -> Result<String, ResponseError> {
190188
<payjoin::send::v1::V1Context as Clone>::clone(&self.0.clone())
191-
.process_response(&mut decoder)
189+
.process_response(response)
192190
.map(|e| e.to_string())
193191
.map_err(Into::into)
194192
}

payjoin-ffi/src/send/uni.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ impl From<super::V1Context> for V1Context {
185185
impl V1Context {
186186
/// Decodes and validates the response.
187187
/// Call this method with response from receiver to continue BIP78 flow. If the response is valid you will get appropriate PSBT that you should sign and broadcast.
188-
pub fn process_response(&self, response: Vec<u8>) -> Result<String, ResponseError> {
188+
pub fn process_response(&self, response: &[u8]) -> Result<String, ResponseError> {
189189
self.0.process_response(response)
190190
}
191191
}

payjoin/src/receive/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ impl From<InternalPayloadError> for PayloadError {
164164
#[derive(Debug)]
165165
pub(crate) enum InternalPayloadError {
166166
/// The payload is not valid utf-8
167-
Utf8(std::string::FromUtf8Error),
167+
Utf8(std::str::Utf8Error),
168168
/// The payload is not a valid PSBT
169169
ParsePsbt(bitcoin::psbt::PsbtParseError),
170170
/// Invalid sender parameters

payjoin/src/receive/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,11 @@ impl<'a> From<&'a InputPair> for InternalInputPair<'a> {
212212

213213
/// Validate the payload of a Payjoin request for PSBT and Params sanity
214214
pub(crate) fn parse_payload(
215-
base64: String,
215+
base64: &str,
216216
query: &str,
217217
supported_versions: &'static [Version],
218218
) -> Result<(Psbt, Params), PayloadError> {
219-
let unchecked_psbt = Psbt::from_str(&base64).map_err(InternalPayloadError::ParsePsbt)?;
219+
let unchecked_psbt = Psbt::from_str(base64).map_err(InternalPayloadError::ParsePsbt)?;
220220

221221
let psbt = unchecked_psbt.validate().map_err(InternalPayloadError::InconsistentPsbt)?;
222222
log::debug!("Received original psbt: {psbt:?}");

payjoin/src/receive/v1/exclusive/error.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ pub struct RequestError(InternalRequestError);
1818

1919
#[derive(Debug)]
2020
pub(crate) enum InternalRequestError {
21-
/// I/O error while reading the request body
22-
Io(std::io::Error),
2321
/// A required HTTP header is missing from the request
2422
MissingHeader(&'static str),
2523
/// The Content-Type header has an invalid value
@@ -43,8 +41,7 @@ impl From<RequestError> for JsonReply {
4341
use InternalRequestError::*;
4442

4543
match &e.0 {
46-
Io(_)
47-
| MissingHeader(_)
44+
MissingHeader(_)
4845
| InvalidContentType(_)
4946
| InvalidContentLength(_)
5047
| ContentLengthTooLarge(_) =>
@@ -56,7 +53,6 @@ impl From<RequestError> for JsonReply {
5653
impl fmt::Display for RequestError {
5754
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
5855
match &self.0 {
59-
InternalRequestError::Io(e) => write!(f, "{e}"),
6056
InternalRequestError::MissingHeader(header) => write!(f, "Missing header: {header}"),
6157
InternalRequestError::InvalidContentType(content_type) =>
6258
write!(f, "Invalid content type: {content_type}"),
@@ -70,7 +66,6 @@ impl fmt::Display for RequestError {
7066
impl error::Error for RequestError {
7167
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
7268
match &self.0 {
73-
InternalRequestError::Io(e) => Some(e),
7469
InternalRequestError::InvalidContentLength(e) => Some(e),
7570
InternalRequestError::MissingHeader(_) => None,
7671
InternalRequestError::InvalidContentType(_) => None,

payjoin/src/receive/v1/exclusive/mod.rs

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ pub fn build_v1_pj_uri<'a>(
2323

2424
impl UncheckedProposal {
2525
pub fn from_request(
26-
body: impl std::io::Read,
26+
body: &[u8],
2727
query: &str,
2828
headers: impl Headers,
2929
) -> Result<Self, ReplyableError> {
30-
let parsed_body = parse_body(headers, body).map_err(ReplyableError::V1)?;
30+
let validated_body = validate_body(headers, body).map_err(ReplyableError::V1)?;
3131

32-
let base64 = String::from_utf8(parsed_body).map_err(InternalPayloadError::Utf8)?;
32+
let base64 = std::str::from_utf8(validated_body).map_err(InternalPayloadError::Utf8)?;
3333

3434
let (psbt, params) = crate::receive::parse_payload(base64, query, SUPPORTED_VERSIONS)
3535
.map_err(ReplyableError::Payload)?;
@@ -41,10 +41,7 @@ impl UncheckedProposal {
4141
/// Validate the request headers for a Payjoin request
4242
///
4343
/// [`RequestError`] should only be produced here.
44-
fn parse_body(
45-
headers: impl Headers,
46-
mut body: impl std::io::Read,
47-
) -> Result<Vec<u8>, RequestError> {
44+
fn validate_body(headers: impl Headers, body: &[u8]) -> Result<&[u8], RequestError> {
4845
let content_type = headers
4946
.get_header("content-type")
5047
.ok_or(InternalRequestError::MissingHeader("Content-Type"))?;
@@ -61,9 +58,7 @@ fn parse_body(
6158
return Err(InternalRequestError::ContentLengthTooLarge(content_length).into());
6259
}
6360

64-
let mut buf = vec![0; content_length];
65-
body.read_exact(&mut buf).map_err(InternalRequestError::Io)?;
66-
Ok(buf)
61+
Ok(&body[..content_length])
6762
}
6863

6964
#[cfg(test)]
@@ -99,9 +94,9 @@ mod tests {
9994
padded_body.resize(MAX_CONTENT_LENGTH + 1, 0);
10095
let headers = MockHeaders::new(padded_body.len() as u64);
10196

102-
let parsed_request = parse_body(headers.clone(), padded_body.as_slice());
103-
assert!(parsed_request.is_err());
104-
match parsed_request {
97+
let validated_request = validate_body(headers.clone(), padded_body.as_slice());
98+
assert!(validated_request.is_err());
99+
match validated_request {
105100
Ok(_) => panic!("Expected error, got success"),
106101
Err(error) => {
107102
assert_eq!(
@@ -119,8 +114,8 @@ mod tests {
119114
fn test_from_request() -> Result<(), Box<dyn std::error::Error>> {
120115
let body = ORIGINAL_PSBT.as_bytes();
121116
let headers = MockHeaders::new(body.len() as u64);
122-
let parsed_request = parse_body(headers.clone(), body);
123-
assert!(parsed_request.is_ok());
117+
let validated_request = validate_body(headers.clone(), body);
118+
assert!(validated_request.is_ok());
124119

125120
let proposal = UncheckedProposal::from_request(body, QUERY_PARAMS, headers)?;
126121

payjoin/src/receive/v2/mod.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ impl Receiver<WithContext> {
188188
Some(body) => body,
189189
None => return Ok(None),
190190
};
191-
match String::from_utf8(body.clone()) {
191+
match std::str::from_utf8(&body) {
192192
// V1 response bodies are utf8 plaintext
193193
Ok(response) => Ok(Some(Receiver { state: self.extract_proposal_from_v1(response)? })),
194194
// V2 response bodies are encrypted binary
@@ -208,28 +208,28 @@ impl Receiver<WithContext> {
208208

209209
fn extract_proposal_from_v1(
210210
&mut self,
211-
response: String,
211+
response: &str,
212212
) -> Result<UncheckedProposal, ReplyableError> {
213213
self.unchecked_from_payload(response)
214214
}
215215

216216
fn extract_proposal_from_v2(&mut self, response: Vec<u8>) -> Result<UncheckedProposal, Error> {
217217
let (payload_bytes, e) = decrypt_message_a(&response, self.context.s.secret_key().clone())?;
218218
self.context.e = Some(e);
219-
let payload = String::from_utf8(payload_bytes)
219+
let payload = std::str::from_utf8(&payload_bytes)
220220
.map_err(|e| Error::ReplyToSender(InternalPayloadError::Utf8(e).into()))?;
221221
self.unchecked_from_payload(payload).map_err(Error::ReplyToSender)
222222
}
223223

224224
fn unchecked_from_payload(
225225
&mut self,
226-
payload: String,
226+
payload: &str,
227227
) -> Result<UncheckedProposal, ReplyableError> {
228228
let (base64, padded_query) = payload.split_once('\n').unwrap_or_default();
229229
let query = padded_query.trim_matches('\0');
230230
log::trace!("Received query: {query}, base64: {base64}"); // my guess is no \n so default is wrong
231-
let (psbt, mut params) = parse_payload(base64.to_string(), query, SUPPORTED_VERSIONS)
232-
.map_err(ReplyableError::Payload)?;
231+
let (psbt, mut params) =
232+
parse_payload(base64, query, SUPPORTED_VERSIONS).map_err(ReplyableError::Payload)?;
233233

234234
// Output substitution must be disabled for V1 sessions in V2 contexts.
235235
//

payjoin/src/send/error.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ pub struct ValidationError(InternalValidationError);
9595
#[derive(Debug)]
9696
pub(crate) enum InternalValidationError {
9797
Parse,
98-
Io(std::io::Error),
9998
ContentTooLarge,
10099
Proposal(InternalProposalError),
101100
#[cfg(feature = "v2")]
@@ -120,7 +119,6 @@ impl fmt::Display for ValidationError {
120119

121120
match &self.0 {
122121
Parse => write!(f, "couldn't decode as PSBT or JSON",),
123-
Io(e) => write!(f, "couldn't read PSBT: {e}"),
124122
ContentTooLarge => write!(f, "content is larger than {MAX_CONTENT_LENGTH} bytes"),
125123
Proposal(e) => write!(f, "proposal PSBT error: {e}"),
126124
#[cfg(feature = "v2")]
@@ -135,7 +133,6 @@ impl std::error::Error for ValidationError {
135133

136134
match &self.0 {
137135
Parse => None,
138-
Io(error) => Some(error),
139136
ContentTooLarge => None,
140137
Proposal(e) => Some(e),
141138
#[cfg(feature = "v2")]

0 commit comments

Comments
 (0)