Skip to content

Commit d581c13

Browse files
committed
Propagate errors when retrieving exp parameter
1 parent 0891a5b commit d581c13

File tree

3 files changed

+64
-18
lines changed

3 files changed

+64
-18
lines changed

payjoin/src/send/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ impl Sender {
285285
ohttp_relay: Url,
286286
) -> Result<(Request, V2PostContext), CreateRequestError> {
287287
use crate::uri::UrlExt;
288-
if let Some(expiry) = self.endpoint.exp() {
288+
if let Ok(expiry) = self.endpoint.exp() {
289289
if std::time::SystemTime::now() > expiry {
290290
return Err(InternalCreateRequestError::Expired(expiry).into());
291291
}

payjoin/src/uri/error.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,29 @@ pub(crate) enum ParseOhttpKeysParamError {
1818
InvalidOhttpKeys(crate::ohttp::ParseOhttpKeysError),
1919
}
2020

21+
#[cfg(feature = "v2")]
22+
#[derive(Debug)]
23+
pub(crate) enum ParseExpParamError {
24+
MissingExp,
25+
InvalidHrp(bitcoin::bech32::Hrp),
26+
DecodeBech32(bitcoin::bech32::primitives::decode::CheckedHrpstringError),
27+
InvalidExp(bitcoin::consensus::encode::Error),
28+
}
29+
30+
#[cfg(feature = "v2")]
31+
impl std::fmt::Display for ParseExpParamError {
32+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33+
use ParseExpParamError::*;
34+
35+
match &self {
36+
MissingExp => write!(f, "exp is missing"),
37+
InvalidHrp(h) => write!(f, "incorrect hrp for exp: {}", h),
38+
DecodeBech32(d) => write!(f, "exp is not valid bech32: {}", d),
39+
InvalidExp(i) => write!(f, "exp param does not contain a bitcoin consensus encoded u32: {}", i),
40+
}
41+
}
42+
}
43+
2144
#[cfg(feature = "v2")]
2245
impl std::fmt::Display for ParseOhttpKeysParamError {
2346
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {

payjoin/src/uri/url_ext.rs

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use bitcoin::consensus::encode::Decodable;
55
use bitcoin::consensus::Encodable;
66
use url::Url;
77

8-
use super::error::{ParseOhttpKeysParamError, ParseReceiverPubkeyParamError};
8+
use super::error::{ParseExpParamError, ParseOhttpKeysParamError, ParseReceiverPubkeyParamError};
99
use crate::hpke::HpkePublicKey;
1010
use crate::ohttp::OhttpKeys;
1111

@@ -15,7 +15,7 @@ pub(crate) trait UrlExt {
1515
fn set_receiver_pubkey(&mut self, exp: HpkePublicKey);
1616
fn ohttp(&self) -> Result<OhttpKeys, ParseOhttpKeysParamError>;
1717
fn set_ohttp(&mut self, ohttp: OhttpKeys);
18-
fn exp(&self) -> Option<std::time::SystemTime>;
18+
fn exp(&self) -> Result<std::time::SystemTime, ParseExpParamError>;
1919
fn set_exp(&mut self, exp: std::time::SystemTime);
2020
}
2121

@@ -60,22 +60,23 @@ impl UrlExt for Url {
6060
fn set_ohttp(&mut self, ohttp: OhttpKeys) { set_param(self, "OH1", &ohttp.to_string()) }
6161

6262
/// Retrieve the exp parameter from the URL fragment
63-
fn exp(&self) -> Option<std::time::SystemTime> {
64-
get_param(self, "EX1", |value| {
65-
let (hrp, bytes) = crate::bech32::nochecksum::decode(value).ok()?;
63+
fn exp(&self) -> Result<std::time::SystemTime, ParseExpParamError> {
64+
let value =
65+
get_param(self, "EX1", |v| Some(v.to_owned())).ok_or(ParseExpParamError::MissingExp)?;
6666

67-
let ex_hrp: Hrp = Hrp::parse("EX").unwrap();
68-
if hrp != ex_hrp {
69-
return None;
70-
}
67+
let (hrp, bytes) =
68+
crate::bech32::nochecksum::decode(&value).map_err(ParseExpParamError::DecodeBech32)?;
7169

72-
let mut cursor = &bytes[..];
73-
u32::consensus_decode(&mut cursor)
74-
.map(|timestamp| {
75-
std::time::UNIX_EPOCH + std::time::Duration::from_secs(timestamp as u64)
76-
})
77-
.ok()
78-
})
70+
let ex_hrp: Hrp = Hrp::parse("EX").unwrap();
71+
if hrp != ex_hrp {
72+
return Err(ParseExpParamError::InvalidHrp(hrp));
73+
}
74+
75+
u32::consensus_decode(&mut &bytes[..])
76+
.map(|timestamp| {
77+
std::time::UNIX_EPOCH + std::time::Duration::from_secs(timestamp as u64)
78+
})
79+
.map_err(ParseExpParamError::InvalidExp)
7980
}
8081

8182
/// Set the exp parameter in the URL fragment
@@ -173,7 +174,29 @@ mod tests {
173174
url.set_exp(exp_time);
174175
assert_eq!(url.fragment(), Some("EX1C4UC6ES"));
175176

176-
assert_eq!(url.exp(), Some(exp_time));
177+
assert_eq!(url.exp().unwrap(), exp_time);
178+
}
179+
180+
#[test]
181+
fn test_errors_when_parsing_exp() {
182+
let missing_exp_url = Url::parse("http://example.com").unwrap();
183+
assert!(matches!(missing_exp_url.exp(), Err(ParseExpParamError::MissingExp)));
184+
185+
let invalid_bech32_exp_url =
186+
Url::parse("http://example.com?pj=https://test-payjoin-url#EX1invalid_bech_32")
187+
.unwrap();
188+
assert!(matches!(invalid_bech32_exp_url.exp(), Err(ParseExpParamError::DecodeBech32(_))));
189+
190+
// Since the HRP is everything to the left of the right-most separator, the invalid url in
191+
// this test would have it's HRP being parsed as EX101 instead of the expected EX1
192+
let invalid_hrp_exp_url =
193+
Url::parse("http://example.com?pj=https://test-payjoin-url#EX1010").unwrap();
194+
assert!(matches!(invalid_hrp_exp_url.exp(), Err(ParseExpParamError::InvalidHrp(_))));
195+
196+
// Not enough data to decode into a u32
197+
let invalid_timestamp_exp_url =
198+
Url::parse("http://example.com?pj=https://test-payjoin-url#EX10").unwrap();
199+
assert!(matches!(invalid_timestamp_exp_url.exp(), Err(ParseExpParamError::InvalidExp(_))))
177200
}
178201

179202
#[test]

0 commit comments

Comments
 (0)