Skip to content

Commit e8f1069

Browse files
authored
Propagate errors when retrieving exp parameter (payjoin#441)
Follow-up of payjoin#428 to propagate errors when retrieving the `exp` parameter. This is the last of the parameter getters that need an update to return a `Result`, so this will close payjoin#399
2 parents 0891a5b + 4ea00db commit e8f1069

File tree

4 files changed

+123
-77
lines changed

4 files changed

+123
-77
lines changed

payjoin/src/send/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use bitcoin::transaction::Version;
55
use bitcoin::{AddressType, Sequence};
66

77
#[cfg(feature = "v2")]
8-
use crate::uri::error::ParseReceiverPubkeyParamError;
8+
use crate::uri::url_ext::ParseReceiverPubkeyParamError;
99

1010
/// Error that may occur when the response from receiver is malformed.
1111
///

payjoin/src/send/mod.rs

Lines changed: 2 additions & 2 deletions
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
}
@@ -331,7 +331,7 @@ impl Sender {
331331
#[cfg(feature = "v2")]
332332
fn extract_rs_pubkey(
333333
&self,
334-
) -> Result<HpkePublicKey, crate::uri::error::ParseReceiverPubkeyParamError> {
334+
) -> Result<HpkePublicKey, crate::uri::url_ext::ParseReceiverPubkeyParamError> {
335335
use crate::uri::UrlExt;
336336
self.endpoint.receiver_pubkey()
337337
}

payjoin/src/uri/error.rs

Lines changed: 0 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,63 +11,6 @@ pub(crate) enum InternalPjParseError {
1111
UnsecureEndpoint,
1212
}
1313

14-
#[cfg(feature = "v2")]
15-
#[derive(Debug)]
16-
pub(crate) enum ParseOhttpKeysParamError {
17-
MissingOhttpKeys,
18-
InvalidOhttpKeys(crate::ohttp::ParseOhttpKeysError),
19-
}
20-
21-
#[cfg(feature = "v2")]
22-
impl std::fmt::Display for ParseOhttpKeysParamError {
23-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24-
use ParseOhttpKeysParamError::*;
25-
26-
match &self {
27-
MissingOhttpKeys => write!(f, "ohttp keys are missing"),
28-
InvalidOhttpKeys(o) => write!(f, "invalid ohttp keys: {}", o),
29-
}
30-
}
31-
}
32-
33-
#[cfg(feature = "v2")]
34-
#[derive(Debug)]
35-
pub(crate) enum ParseReceiverPubkeyParamError {
36-
MissingPubkey,
37-
InvalidHrp(bitcoin::bech32::Hrp),
38-
DecodeBech32(bitcoin::bech32::primitives::decode::CheckedHrpstringError),
39-
InvalidPubkey(crate::hpke::HpkeError),
40-
}
41-
42-
#[cfg(feature = "v2")]
43-
impl std::fmt::Display for ParseReceiverPubkeyParamError {
44-
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
45-
use ParseReceiverPubkeyParamError::*;
46-
47-
match &self {
48-
MissingPubkey => write!(f, "receiver public key is missing"),
49-
InvalidHrp(h) => write!(f, "incorrect hrp for receiver key: {}", h),
50-
DecodeBech32(e) => write!(f, "receiver public is not valid base64: {}", e),
51-
InvalidPubkey(e) =>
52-
write!(f, "receiver public key does not represent a valid pubkey: {}", e),
53-
}
54-
}
55-
}
56-
57-
#[cfg(feature = "v2")]
58-
impl std::error::Error for ParseReceiverPubkeyParamError {
59-
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
60-
use ParseReceiverPubkeyParamError::*;
61-
62-
match &self {
63-
MissingPubkey => None,
64-
InvalidHrp(_) => None,
65-
DecodeBech32(error) => Some(error),
66-
InvalidPubkey(error) => Some(error),
67-
}
68-
}
69-
}
70-
7114
impl From<InternalPjParseError> for PjParseError {
7215
fn from(value: InternalPjParseError) -> Self { PjParseError(value) }
7316
}

payjoin/src/uri/url_ext.rs

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

8-
use super::error::{ParseOhttpKeysParamError, ParseReceiverPubkeyParamError};
98
use crate::hpke::HpkePublicKey;
109
use crate::ohttp::OhttpKeys;
1110

@@ -15,7 +14,7 @@ pub(crate) trait UrlExt {
1514
fn set_receiver_pubkey(&mut self, exp: HpkePublicKey);
1615
fn ohttp(&self) -> Result<OhttpKeys, ParseOhttpKeysParamError>;
1716
fn set_ohttp(&mut self, ohttp: OhttpKeys);
18-
fn exp(&self) -> Option<std::time::SystemTime>;
17+
fn exp(&self) -> Result<std::time::SystemTime, ParseExpParamError>;
1918
fn set_exp(&mut self, exp: std::time::SystemTime);
2019
}
2120

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

6261
/// 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()?;
62+
fn exp(&self) -> Result<std::time::SystemTime, ParseExpParamError> {
63+
let value =
64+
get_param(self, "EX1", |v| Some(v.to_owned())).ok_or(ParseExpParamError::MissingExp)?;
6665

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

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-
})
69+
let ex_hrp: Hrp = Hrp::parse("EX").unwrap();
70+
if hrp != ex_hrp {
71+
return Err(ParseExpParamError::InvalidHrp(hrp));
72+
}
73+
74+
u32::consensus_decode(&mut &bytes[..])
75+
.map(|timestamp| {
76+
std::time::UNIX_EPOCH + std::time::Duration::from_secs(timestamp as u64)
77+
})
78+
.map_err(ParseExpParamError::InvalidExp)
7979
}
8080

8181
/// Set the exp parameter in the URL fragment
@@ -130,6 +130,87 @@ fn set_param(url: &mut Url, prefix: &str, param: &str) {
130130
url.set_fragment(if fragment.is_empty() { None } else { Some(&fragment) });
131131
}
132132

133+
#[cfg(feature = "v2")]
134+
#[derive(Debug)]
135+
pub(crate) enum ParseOhttpKeysParamError {
136+
MissingOhttpKeys,
137+
InvalidOhttpKeys(crate::ohttp::ParseOhttpKeysError),
138+
}
139+
140+
#[cfg(feature = "v2")]
141+
impl std::fmt::Display for ParseOhttpKeysParamError {
142+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143+
use ParseOhttpKeysParamError::*;
144+
145+
match &self {
146+
MissingOhttpKeys => write!(f, "ohttp keys are missing"),
147+
InvalidOhttpKeys(o) => write!(f, "invalid ohttp keys: {}", o),
148+
}
149+
}
150+
}
151+
152+
#[cfg(feature = "v2")]
153+
#[derive(Debug)]
154+
pub(crate) enum ParseExpParamError {
155+
MissingExp,
156+
InvalidHrp(bitcoin::bech32::Hrp),
157+
DecodeBech32(bitcoin::bech32::primitives::decode::CheckedHrpstringError),
158+
InvalidExp(bitcoin::consensus::encode::Error),
159+
}
160+
161+
#[cfg(feature = "v2")]
162+
impl std::fmt::Display for ParseExpParamError {
163+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164+
use ParseExpParamError::*;
165+
166+
match &self {
167+
MissingExp => write!(f, "exp is missing"),
168+
InvalidHrp(h) => write!(f, "incorrect hrp for exp: {}", h),
169+
DecodeBech32(d) => write!(f, "exp is not valid bech32: {}", d),
170+
InvalidExp(i) =>
171+
write!(f, "exp param does not contain a bitcoin consensus encoded u32: {}", i),
172+
}
173+
}
174+
}
175+
176+
#[cfg(feature = "v2")]
177+
#[derive(Debug)]
178+
pub(crate) enum ParseReceiverPubkeyParamError {
179+
MissingPubkey,
180+
InvalidHrp(bitcoin::bech32::Hrp),
181+
DecodeBech32(bitcoin::bech32::primitives::decode::CheckedHrpstringError),
182+
InvalidPubkey(crate::hpke::HpkeError),
183+
}
184+
185+
#[cfg(feature = "v2")]
186+
impl std::fmt::Display for ParseReceiverPubkeyParamError {
187+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
188+
use ParseReceiverPubkeyParamError::*;
189+
190+
match &self {
191+
MissingPubkey => write!(f, "receiver public key is missing"),
192+
InvalidHrp(h) => write!(f, "incorrect hrp for receiver key: {}", h),
193+
DecodeBech32(e) => write!(f, "receiver public is not valid base64: {}", e),
194+
InvalidPubkey(e) =>
195+
write!(f, "receiver public key does not represent a valid pubkey: {}", e),
196+
}
197+
}
198+
}
199+
200+
#[cfg(feature = "v2")]
201+
impl std::error::Error for ParseReceiverPubkeyParamError {
202+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
203+
use ParseReceiverPubkeyParamError::*;
204+
205+
match &self {
206+
MissingPubkey => None,
207+
InvalidHrp(_) => None,
208+
DecodeBech32(error) => Some(error),
209+
InvalidPubkey(error) => Some(error),
210+
}
211+
}
212+
}
213+
133214
#[cfg(test)]
134215
mod tests {
135216
use super::*;
@@ -173,7 +254,29 @@ mod tests {
173254
url.set_exp(exp_time);
174255
assert_eq!(url.fragment(), Some("EX1C4UC6ES"));
175256

176-
assert_eq!(url.exp(), Some(exp_time));
257+
assert_eq!(url.exp().unwrap(), exp_time);
258+
}
259+
260+
#[test]
261+
fn test_errors_when_parsing_exp() {
262+
let missing_exp_url = Url::parse("http://example.com").unwrap();
263+
assert!(matches!(missing_exp_url.exp(), Err(ParseExpParamError::MissingExp)));
264+
265+
let invalid_bech32_exp_url =
266+
Url::parse("http://example.com?pj=https://test-payjoin-url#EX1invalid_bech_32")
267+
.unwrap();
268+
assert!(matches!(invalid_bech32_exp_url.exp(), Err(ParseExpParamError::DecodeBech32(_))));
269+
270+
// Since the HRP is everything to the left of the right-most separator, the invalid url in
271+
// this test would have it's HRP being parsed as EX101 instead of the expected EX1
272+
let invalid_hrp_exp_url =
273+
Url::parse("http://example.com?pj=https://test-payjoin-url#EX1010").unwrap();
274+
assert!(matches!(invalid_hrp_exp_url.exp(), Err(ParseExpParamError::InvalidHrp(_))));
275+
276+
// Not enough data to decode into a u32
277+
let invalid_timestamp_exp_url =
278+
Url::parse("http://example.com?pj=https://test-payjoin-url#EX10").unwrap();
279+
assert!(matches!(invalid_timestamp_exp_url.exp(), Err(ParseExpParamError::InvalidExp(_))))
177280
}
178281

179282
#[test]

0 commit comments

Comments
 (0)