Skip to content

Commit

Permalink
Add an option to not validate the success action url
Browse files Browse the repository at this point in the history
  • Loading branch information
dangeross committed Jul 15, 2024
1 parent b1003e7 commit ef63ea4
Show file tree
Hide file tree
Showing 13 changed files with 192 additions and 17 deletions.
1 change: 1 addition & 0 deletions libs/sdk-bindings/src/breez_sdk.udl
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,7 @@ dictionary LnUrlPayRequest {
u64 amount_msat;
string? comment = null;
string? payment_label = null;
boolean? validate_success_action_url = null;
};

dictionary LnUrlPayRequestData {
Expand Down
38 changes: 31 additions & 7 deletions libs/sdk-common/src/lnurl/specs/pay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub async fn validate_lnurl_pay(
comment: &Option<String>,
req_data: &LnUrlPayRequestData,
network: Network,
validate_success_action_url: Option<bool>,
) -> LnUrlResult<ValidatedCallbackResponse> {
validate_user_input(
user_amount_msat,
Expand All @@ -37,8 +38,9 @@ pub async fn validate_lnurl_pay(
SuccessAction::Aes(data) => data.validate()?,
SuccessAction::Message(data) => data.validate()?,
SuccessAction::Url(data) => {
callback_resp.success_action =
Some(SuccessAction::Url(data.validate(req_data)?));
callback_resp.success_action = Some(SuccessAction::Url(
data.validate(req_data, validate_success_action_url.unwrap_or(true))?,
));
}
}
}
Expand Down Expand Up @@ -134,6 +136,9 @@ pub mod model {
pub comment: Option<String>,
/// The external label or identifier of the [Payment]
pub payment_label: Option<String>,
/// Validates that, if there is a URL success action, the URL domain matches
/// the LNURL callback domain. Defaults to `true`
pub validate_success_action_url: Option<bool>,
}

pub enum ValidatedCallbackResponse {
Expand Down Expand Up @@ -321,7 +326,11 @@ pub mod model {
}

impl UrlSuccessActionData {
pub fn validate(&self, data: &LnUrlPayRequestData) -> LnUrlResult<UrlSuccessActionData> {
pub fn validate(
&self,
data: &LnUrlPayRequestData,
validate_url: bool,
) -> LnUrlResult<UrlSuccessActionData> {
let mut validated_data = self.clone();
match self.description.len() <= 144 {
true => Ok(()),
Expand All @@ -342,6 +351,12 @@ pub mod model {
LnUrlError::invalid_uri("Could not determine Success Action URL domain")
})?;

if validate_url && req_domain != action_res_domain {
return Err(LnUrlError::generic(
"Success Action URL has different domain than the callback domain",
));
}

validated_data.matches_callback_domain = req_domain == action_res_domain;
Ok(validated_data)
})
Expand Down Expand Up @@ -620,17 +635,26 @@ pub(crate) mod tests {
url: pay_req_data.callback.clone(),
matches_callback_domain: true,
}
.validate(&pay_req_data);
.validate(&pay_req_data, true);
assert!(validated_data1.is_ok());
assert!(validated_data1.unwrap().matches_callback_domain);

// Different Success Action domain than in the callback URL
// Different Success Action domain than in the callback URL with validation
assert!(UrlSuccessActionData {
description: "short msg".into(),
url: "https://new-domain.com/test-url".into(),
matches_callback_domain: true,
}
.validate(&pay_req_data, true)
.is_err());

// Different Success Action domain than in the callback URL without validation
let validated_data2 = UrlSuccessActionData {
description: "short msg".into(),
url: "https://new-domain.com/test-url".into(),
matches_callback_domain: true,
}
.validate(&pay_req_data);
.validate(&pay_req_data, false);
assert!(validated_data2.is_ok());
assert!(!validated_data2.unwrap().matches_callback_domain);

Expand All @@ -640,7 +664,7 @@ pub(crate) mod tests {
url: pay_req_data.callback.clone(),
matches_callback_domain: true,
}
.validate(&pay_req_data)
.validate(&pay_req_data, true)
.is_err());

Ok(())
Expand Down
1 change: 1 addition & 0 deletions libs/sdk-core/src/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ pub struct _LnUrlPayRequest {
pub amount_msat: u64,
pub comment: Option<String>,
pub payment_label: Option<String>,
pub validate_success_action_url: Option<bool>,
}

#[frb(mirror(LnUrlPayRequestData))]
Expand Down
1 change: 1 addition & 0 deletions libs/sdk-core/src/breez_services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ impl BreezServices {
&req.comment,
&req.data,
self.config.network,
req.validate_success_action_url,
)
.await?
{
Expand Down
3 changes: 3 additions & 0 deletions libs/sdk-core/src/bridge_generated.io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,7 @@ impl Wire2Api<LnUrlPayRequest> for wire_LnUrlPayRequest {
amount_msat: self.amount_msat.wire2api(),
comment: self.comment.wire2api(),
payment_label: self.payment_label.wire2api(),
validate_success_action_url: self.validate_success_action_url.wire2api(),
}
}
}
Expand Down Expand Up @@ -1237,6 +1238,7 @@ pub struct wire_LnUrlPayRequest {
amount_msat: u64,
comment: *mut wire_uint_8_list,
payment_label: *mut wire_uint_8_list,
validate_success_action_url: *mut bool,
}

#[repr(C)]
Expand Down Expand Up @@ -1644,6 +1646,7 @@ impl NewWithNullPtr for wire_LnUrlPayRequest {
amount_msat: Default::default(),
comment: core::ptr::null_mut(),
payment_label: core::ptr::null_mut(),
validate_success_action_url: core::ptr::null_mut(),
}
}
}
Expand Down
124 changes: 116 additions & 8 deletions libs/sdk-core/src/lnurl/pay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ pub(crate) mod tests {
/// Mock an LNURL-pay endpoint that responds with a Success Action of type URL
fn mock_lnurl_pay_callback_endpoint_url_success_action(
callback_params: LnurlPayCallbackParams,
success_action_url: Option<&str>,
) -> Result<Mock> {
let LnurlPayCallbackParams {
pay_req,
Expand All @@ -215,14 +216,18 @@ pub(crate) mod tests {
"successAction": {
"tag":"url",
"description":"test description",
"url":"http://localhost:8080/test-url"
"url":"success-action-url"
}
}
"#
.replace('\n', "")
.replace(
"token-invoice",
&pr.unwrap_or_else(|| "token-invoice".to_string()),
)
.replace(
"success-action-url",
success_action_url.unwrap_or("http://localhost:8080/test-url"),
);

let response_body = match error {
Expand Down Expand Up @@ -399,6 +404,7 @@ pub(crate) mod tests {
amount_msat: user_amount_msat,
comment: Some(comment),
payment_label: None,
validate_success_action_url: None,
})
.await?
{
Expand Down Expand Up @@ -443,6 +449,7 @@ pub(crate) mod tests {
amount_msat: user_amount_msat,
comment: Some(comment),
payment_label: None,
validate_success_action_url: None,
})
.await;
// An unsupported Success Action results in an error
Expand Down Expand Up @@ -473,6 +480,7 @@ pub(crate) mod tests {
amount_msat: user_amount_msat,
comment: Some(comment),
payment_label: None,
validate_success_action_url: None,
})
.await?
{
Expand Down Expand Up @@ -506,6 +514,7 @@ pub(crate) mod tests {
amount_msat: user_amount_msat,
comment: Some(comment),
payment_label: None,
validate_success_action_url: None,
})
.await?
{
Expand Down Expand Up @@ -554,6 +563,7 @@ pub(crate) mod tests {
amount_msat: user_amount_msat,
comment: Some(comment),
payment_label: None,
validate_success_action_url: None,
})
.await
.is_err());
Expand Down Expand Up @@ -584,6 +594,7 @@ pub(crate) mod tests {
amount_msat: user_amount_msat,
comment: Some(comment),
payment_label: None,
validate_success_action_url: None,
})
.await;
assert!(matches!(res, Ok(LnUrlPayResult::EndpointError { data: _ })));
Expand All @@ -606,13 +617,16 @@ pub(crate) mod tests {
let temp_desc = pay_req.metadata_str.clone();
let inv = rand_invoice_with_description_hash(temp_desc)?;
let user_amount_msat = inv.amount_milli_satoshis().unwrap();
let _m = mock_lnurl_pay_callback_endpoint_url_success_action(LnurlPayCallbackParams {
pay_req: &pay_req,
user_amount_msat,
error: None,
pr: Some(inv.to_string()),
comment: comment.clone(),
})?;
let _m = mock_lnurl_pay_callback_endpoint_url_success_action(
LnurlPayCallbackParams {
pay_req: &pay_req,
user_amount_msat,
error: None,
pr: Some(inv.to_string()),
comment: comment.clone(),
},
None,
)?;

let mock_breez_services = crate::breez_services::tests::breez_services().await?;
match mock_breez_services
Expand All @@ -621,6 +635,7 @@ pub(crate) mod tests {
amount_msat: user_amount_msat,
comment: Some(comment),
payment_label: None,
validate_success_action_url: None,
})
.await?
{
Expand Down Expand Up @@ -652,6 +667,97 @@ pub(crate) mod tests {
}
}

#[tokio::test]
async fn test_lnurl_pay_url_success_action_validate_url_invalid() -> Result<()> {
let comment = rand_string(COMMENT_LENGTH as usize);
let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
let temp_desc = pay_req.metadata_str.clone();
let inv = rand_invoice_with_description_hash(temp_desc)?;
let user_amount_msat = inv.amount_milli_satoshis().unwrap();
let _m = mock_lnurl_pay_callback_endpoint_url_success_action(
LnurlPayCallbackParams {
pay_req: &pay_req,
user_amount_msat,
error: None,
pr: Some(inv.to_string()),
comment: comment.clone(),
},
Some("http://different.localhost:8080/test-url"),
)?;

let mock_breez_services = crate::breez_services::tests::breez_services().await?;
let r = mock_breez_services
.lnurl_pay(LnUrlPayRequest {
data: pay_req,
amount_msat: user_amount_msat,
comment: Some(comment),
payment_label: None,
validate_success_action_url: Some(true),
})
.await;
// An invalid Success Action URL results in an error
assert!(r.is_err());

Ok(())
}

#[tokio::test]
async fn test_lnurl_pay_url_success_action_validate_url_valid() -> Result<()> {
let comment = rand_string(COMMENT_LENGTH as usize);
let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
let temp_desc = pay_req.metadata_str.clone();
let inv = rand_invoice_with_description_hash(temp_desc)?;
let user_amount_msat = inv.amount_milli_satoshis().unwrap();
let _m = mock_lnurl_pay_callback_endpoint_url_success_action(
LnurlPayCallbackParams {
pay_req: &pay_req,
user_amount_msat,
error: None,
pr: Some(inv.to_string()),
comment: comment.clone(),
},
Some("http://different.localhost:8080/test-url"),
)?;

let mock_breez_services = crate::breez_services::tests::breez_services().await?;
match mock_breez_services
.lnurl_pay(LnUrlPayRequest {
data: pay_req,
amount_msat: user_amount_msat,
comment: Some(comment),
payment_label: None,
validate_success_action_url: Some(false),
})
.await?
{
LnUrlPayResult::EndpointSuccess {
data:
LnUrlPaySuccessData {
success_action: Some(SuccessActionProcessed::Url { data: url }),
..
},
} => {
if url.url == "http://different.localhost:8080/test-url"
&& url.description == "test description"
{
Ok(())
} else {
Err(anyhow!("Unexpected success action content"))
}
}
LnUrlPayResult::EndpointSuccess {
data:
LnUrlPaySuccessData {
success_action: None,
..
},
} => Err(anyhow!(
"Expected success action in callback, but none provided"
)),
_ => Err(anyhow!("Unexpected success action type")),
}
}

#[tokio::test]
async fn test_lnurl_pay_aes_success_action() -> Result<()> {
// Expected fields in the AES payload
Expand Down Expand Up @@ -707,6 +813,7 @@ pub(crate) mod tests {
amount_msat: user_amount_msat,
comment: Some(comment),
payment_label: None,
validate_success_action_url: None,
})
.await?
{
Expand Down Expand Up @@ -791,6 +898,7 @@ pub(crate) mod tests {
amount_msat: user_amount_msat,
comment: Some(comment),
payment_label: None,
validate_success_action_url: None,
})
.await?
{
Expand Down
1 change: 1 addition & 0 deletions libs/sdk-flutter/ios/Classes/bridge_generated.h
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ typedef struct wire_LnUrlPayRequest {
uint64_t amount_msat;
struct wire_uint_8_list *comment;
struct wire_uint_8_list *payment_label;
bool *validate_success_action_url;
} wire_LnUrlPayRequest;

typedef struct wire_LnUrlWithdrawRequestData {
Expand Down
Loading

0 comments on commit ef63ea4

Please sign in to comment.