Skip to content

Commit c13d401

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

File tree

3 files changed

+62
-18
lines changed

3 files changed

+62
-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, "invalid exp: {}", 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: 38 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,24 @@ 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)?;
69+
70+
let ex_hrp: Hrp = Hrp::parse("EX").unwrap();
71+
if hrp != ex_hrp {
72+
return Err(ParseExpParamError::InvalidHrp(hrp));
73+
}
7174

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-
})
75+
let mut cursor = &bytes[..];
76+
u32::consensus_decode(&mut cursor)
77+
.map(|timestamp| {
78+
std::time::UNIX_EPOCH + std::time::Duration::from_secs(timestamp as u64)
79+
})
80+
.map_err(|e| ParseExpParamError::InvalidExp(e))
7981
}
8082

8183
/// Set the exp parameter in the URL fragment
@@ -173,7 +175,26 @@ mod tests {
173175
url.set_exp(exp_time);
174176
assert_eq!(url.fragment(), Some("EX1C4UC6ES"));
175177

176-
assert_eq!(url.exp(), Some(exp_time));
178+
assert_eq!(url.exp().unwrap(), exp_time);
179+
}
180+
181+
#[test]
182+
fn test_errors_when_parsing_exp() {
183+
let missing_exp_url = Url::parse("http://example.com").unwrap();
184+
assert!(matches!(missing_exp_url.exp(), Err(ParseExpParamError::MissingExp)));
185+
186+
let invalid_bech32_exp_url =
187+
Url::parse("http://example.com?pj=https://test-payjoin-url#EX1invalid_bech_32")
188+
.unwrap();
189+
assert!(matches!(invalid_bech32_exp_url.exp(), Err(ParseExpParamError::DecodeBech32(_))));
190+
191+
let invalid_hrp_exp_url =
192+
Url::parse("http://example.com?pj=https://test-payjoin-url#EX1010").unwrap();
193+
assert!(matches!(invalid_hrp_exp_url.exp(), Err(ParseExpParamError::InvalidHrp(_))));
194+
195+
let invalid_timestamp_exp_url =
196+
Url::parse("http://example.com?pj=https://test-payjoin-url#EX10").unwrap();
197+
assert!(matches!(invalid_timestamp_exp_url.exp(), Err(ParseExpParamError::InvalidExp(_))))
177198
}
178199

179200
#[test]

0 commit comments

Comments
 (0)