Skip to content

Commit

Permalink
Merge pull request #1039 from breez/savage-lnurl-url-action
Browse files Browse the repository at this point in the history
Add `matches_callback_domain` flag to LNURL URL success action
  • Loading branch information
dangeross authored Jul 15, 2024
2 parents 6a18553 + ef63ea4 commit 618cc39
Show file tree
Hide file tree
Showing 14 changed files with 235 additions and 30 deletions.
2 changes: 2 additions & 0 deletions libs/sdk-bindings/src/breez_sdk.udl
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ dictionary MessageSuccessActionData {
dictionary UrlSuccessActionData {
string description;
string url;
boolean matches_callback_domain;
};

[Enum]
Expand Down Expand Up @@ -626,6 +627,7 @@ dictionary LnUrlPayRequest {
u64 amount_msat;
string? comment = null;
string? payment_label = null;
boolean? validate_success_action_url = null;
};

dictionary LnUrlPayRequestData {
Expand Down
77 changes: 58 additions & 19 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 @@ -31,12 +32,16 @@ pub async fn validate_lnurl_pay(
if let Ok(err) = serde_json::from_str::<LnUrlErrorData>(&callback_resp_text) {
Ok(ValidatedCallbackResponse::EndpointError { data: err })
} else {
let callback_resp: CallbackResponse = serde_json::from_str(&callback_resp_text)?;
let mut callback_resp: CallbackResponse = serde_json::from_str(&callback_resp_text)?;
if let Some(ref sa) = callback_resp.success_action {
match sa {
SuccessAction::Aes(data) => data.validate()?,
SuccessAction::Message(data) => data.validate()?,
SuccessAction::Url(data) => data.validate(req_data)?,
SuccessAction::Url(data) => {
callback_resp.success_action = Some(SuccessAction::Url(
data.validate(req_data, validate_success_action_url.unwrap_or(true))?,
));
}
}
}

Expand Down Expand Up @@ -115,6 +120,7 @@ pub mod model {
use anyhow::Result;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use utils::default_true;

use crate::prelude::specs::pay::{Aes256CbcDec, Aes256CbcEnc};
use crate::prelude::*;
Expand All @@ -130,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 @@ -194,8 +203,17 @@ pub mod model {

#[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
pub struct UrlSuccessActionData {
/// Contents description, up to 144 characters
pub description: String,

/// URL of the success action
pub url: String,

/// Indicates the success URL domain matches the LNURL callback domain.
///
/// See <https://github.com/lnurl/luds/blob/luds/09.md>
#[serde(default = "default_true")]
pub matches_callback_domain: bool,
}

/// [SuccessAction] where contents are ready to be consumed by the caller
Expand Down Expand Up @@ -308,7 +326,12 @@ pub mod model {
}

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

match req_domain == action_res_domain {
true => Ok(()),
false => Err(LnUrlError::generic(
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 @@ -605,27 +630,41 @@ pub(crate) mod tests {
fn test_lnurl_pay_validate_success_url() -> Result<()> {
let pay_req_data = get_test_pay_req_data(0, 100_000, 100);

assert!(UrlSuccessActionData {
let validated_data1 = UrlSuccessActionData {
description: "short msg".into(),
url: pay_req_data.callback.clone()
url: pay_req_data.callback.clone(),
matches_callback_domain: true,
}
.validate(&pay_req_data)
.is_ok());
.validate(&pay_req_data, true);
assert!(validated_data1.is_ok());
assert!(validated_data1.unwrap().matches_callback_domain);

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

// Different Success Action domain than in the callback URL
assert!(UrlSuccessActionData {
// 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()
url: "https://new-domain.com/test-url".into(),
matches_callback_domain: true,
}
.validate(&pay_req_data, false);
assert!(validated_data2.is_ok());
assert!(!validated_data2.unwrap().matches_callback_domain);

// Too long description
assert!(UrlSuccessActionData {
description: rand_string(150),
url: pay_req_data.callback.clone(),
matches_callback_domain: true,
}
.validate(&pay_req_data)
.validate(&pay_req_data, true)
.is_err());

Ok(())
Expand Down
4 changes: 4 additions & 0 deletions libs/sdk-common/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ macro_rules! ensure_sdk {
}
};
}

pub(crate) fn default_true() -> bool {
true
}
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 @@ -338,6 +338,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
Loading

0 comments on commit 618cc39

Please sign in to comment.