Skip to content

Commit

Permalink
Work around '#' escaping bug in bip21 crate
Browse files Browse the repository at this point in the history
The `pj` parameter of the BIP 21 URL is itself a URL which contains a
fragment.

This is not escaped by bip21 during serialization, which according to
RFC 3986 truncates the `pj` parameter, making everything that follows
part of the fragment.

Deserialization likewise ignores #, parsing it as part of the value
which round trips with the incorrect serialization behavior.
  • Loading branch information
nothingmuch committed Oct 21, 2024
1 parent a9b9a34 commit 874733e
Show file tree
Hide file tree
Showing 3 changed files with 13 additions and 2 deletions.
1 change: 1 addition & 0 deletions payjoin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ reqwest = { version = "0.12", default-features = false, optional = true }
rustls = { version = "0.22.4", optional = true }
url = "2.2.2"
serde_json = "1.0.108"
percent-encoding-rfc3986 = "0.1.3"

[dev-dependencies]
bitcoind = { version = "0.36.0", features = ["0_21_2"] }
Expand Down
5 changes: 4 additions & 1 deletion payjoin/src/uri/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::borrow::Cow;
use bitcoin::address::NetworkChecked;
use bitcoin::{Address, Amount};
pub use error::PjParseError;
use percent_encoding_rfc3986::{utf8_percent_encode, AsciiSet, CONTROLS};
use url::Url;

use crate::uri::error::InternalPjParseError;
Expand Down Expand Up @@ -192,14 +193,16 @@ impl<'a> bip21::SerializeParams for &'a MaybePayjoinExtras {
}
}

const FRAGMENT_SEPARATOR: &AsciiSet = &CONTROLS.add(b'#');

impl<'a> bip21::SerializeParams for &'a PayjoinExtras {
type Key = &'static str;
type Value = String;
type Iterator = std::vec::IntoIter<(Self::Key, Self::Value)>;

fn serialize_params(self) -> Self::Iterator {
vec![
("pj", self.endpoint.as_str().to_string()),
("pj", utf8_percent_encode(self.endpoint.as_str(), FRAGMENT_SEPARATOR).to_string()), // FIXME remove work around bip21::Uri not escaping '#' in query parameter values once fixed upstream
("pjos", if self.disable_output_substitution { "1" } else { "0" }.to_string()),
]
.into_iter()
Expand Down
9 changes: 8 additions & 1 deletion payjoin/src/uri/url_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,16 @@ mod tests {

#[test]
fn test_valid_v2_url_fragment_on_bip21() {
let uri = "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?amount=0.01\
&pjos=0&pj=https://example.com\
%23ohttp%3DAQO6SMScPUqSo60A7MY6Ak2hDO0CGAxz7BLYp60syRu0gw";
let uri = Uri::try_from(uri).unwrap().assume_checked().check_pj_supported().unwrap();
assert!(uri.extras.endpoint().ohttp().is_some());

let uri = "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?amount=0.01\
&pj=https://example.com\
#ohttp%3DAQO6SMScPUqSo60A7MY6Ak2hDO0CGAxz7BLYp60syRu0gw";
%23ohttp%3DAQO6SMScPUqSo60A7MY6Ak2hDO0CGAxz7BLYp60syRu0gw\
&pjos=0";
let uri = Uri::try_from(uri).unwrap().assume_checked().check_pj_supported().unwrap();
assert!(uri.extras.endpoint().ohttp().is_some());
}
Expand Down

0 comments on commit 874733e

Please sign in to comment.