Skip to content

Commit

Permalink
Handle fragments with uri::UrlExt trait
Browse files Browse the repository at this point in the history
This extension trait defines functions to parse and set the ohttp parameter
in the fragment of a `pj=` URL.

Close #298
  • Loading branch information
DanGould committed Jul 9, 2024
1 parent 8b097ce commit 9dbdd97
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 153 deletions.
54 changes: 45 additions & 9 deletions payjoin/src/send/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,7 @@ pub(crate) enum InternalCreateRequestError {
#[cfg(feature = "v2")]
OhttpEncapsulation(crate::v2::OhttpEncapsulationError),
#[cfg(feature = "v2")]
SubdirectoryNotBase64(bitcoin::base64::DecodeError),
#[cfg(feature = "v2")]
SubdirectoryInvalidPubkey(bitcoin::secp256k1::Error),
ParseSubdirectory(ParseSubdirectoryError),
#[cfg(feature = "v2")]
MissingOhttpConfig,
}
Expand Down Expand Up @@ -223,9 +221,7 @@ impl fmt::Display for CreateRequestError {
#[cfg(feature = "v2")]
OhttpEncapsulation(e) => write!(f, "v2 error: {}", e),
#[cfg(feature = "v2")]
SubdirectoryNotBase64(e) => write!(f, "subdirectory is not valid base64 error: {}", e),
#[cfg(feature = "v2")]
SubdirectoryInvalidPubkey(e) => write!(f, "subdirectory does not represent a valid pubkey: {}", e),
ParseSubdirectory(e) => write!(f, "cannot parse subdirectory: {}", e),
#[cfg(feature = "v2")]
MissingOhttpConfig => write!(f, "no ohttp configuration with which to make a v2 request available"),
}
Expand Down Expand Up @@ -256,9 +252,7 @@ impl std::error::Error for CreateRequestError {
#[cfg(feature = "v2")]
OhttpEncapsulation(error) => Some(error),
#[cfg(feature = "v2")]
SubdirectoryNotBase64(error) => Some(error),
#[cfg(feature = "v2")]
SubdirectoryInvalidPubkey(error) => Some(error),
ParseSubdirectory(error) => Some(error),
#[cfg(feature = "v2")]
MissingOhttpConfig => None,
}
Expand All @@ -269,6 +263,48 @@ impl From<InternalCreateRequestError> for CreateRequestError {
fn from(value: InternalCreateRequestError) -> Self { CreateRequestError(value) }
}

#[cfg(feature = "v2")]
impl From<ParseSubdirectoryError> for CreateRequestError {
fn from(value: ParseSubdirectoryError) -> Self {
CreateRequestError(InternalCreateRequestError::ParseSubdirectory(value))
}
}

#[cfg(feature = "v2")]
#[derive(Debug)]
pub(crate) enum ParseSubdirectoryError {
MissingSubdirectory,
SubdirectoryNotBase64(bitcoin::base64::DecodeError),
SubdirectoryInvalidPubkey(bitcoin::secp256k1::Error),
}

#[cfg(feature = "v2")]
impl std::fmt::Display for ParseSubdirectoryError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use ParseSubdirectoryError::*;

match &self {
MissingSubdirectory => write!(f, "subdirectory is missing"),
SubdirectoryNotBase64(e) => write!(f, "subdirectory is not valid base64: {}", e),
SubdirectoryInvalidPubkey(e) =>
write!(f, "subdirectory does not represent a valid pubkey: {}", e),
}
}
}

#[cfg(feature = "v2")]
impl std::error::Error for ParseSubdirectoryError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
use ParseSubdirectoryError::*;

match &self {
MissingSubdirectory => None,
SubdirectoryNotBase64(error) => Some(error),
SubdirectoryInvalidPubkey(error) => Some(error),
}
}
}

/// Represent an error returned by Payjoin receiver.
pub enum ResponseError {
/// `WellKnown` Errors are defined in the [`BIP78::ReceiverWellKnownError`] spec.
Expand Down
65 changes: 22 additions & 43 deletions payjoin/src/send/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,6 @@ impl<'a> RequestBuilder<'a> {
psbt.validate_input_utxos(true)
.map_err(InternalCreateRequestError::InvalidOriginalInput)?;
let endpoint = self.uri.extras.endpoint.clone();
#[cfg(feature = "v2")]
let ohttp_keys = self.uri.extras.ohttp_keys;
let disable_output_substitution =
self.uri.extras.disable_output_substitution || self.disable_output_substitution;
let payee = self.uri.address.script_pubkey();
Expand Down Expand Up @@ -234,8 +232,6 @@ impl<'a> RequestBuilder<'a> {
Ok(RequestContext {
psbt,
endpoint,
#[cfg(feature = "v2")]
ohttp_keys,
disable_output_substitution,
fee_contribution,
payee,
Expand All @@ -252,8 +248,6 @@ impl<'a> RequestBuilder<'a> {
pub struct RequestContext {
psbt: Psbt,
endpoint: Url,
#[cfg(feature = "v2")]
ohttp_keys: Option<crate::v2::OhttpKeys>,
disable_output_substitution: bool,
fee_contribution: Option<(bitcoin::Amount, usize)>,
min_fee_rate: FeeRate,
Expand All @@ -271,7 +265,7 @@ impl RequestContext {
/// Extract serialized V1 Request and Context froma Payjoin Proposal
pub fn extract_v1(self) -> Result<(Request, ContextV1), CreateRequestError> {
let url = serialize_url(
self.endpoint.into(),
self.endpoint,
self.disable_output_substitution,
self.fee_contribution,
self.min_fee_rate,
Expand Down Expand Up @@ -303,6 +297,7 @@ impl RequestContext {
&mut self,
ohttp_relay: Url,
) -> Result<(Request, ContextV2), CreateRequestError> {
use crate::uri::UrlExt;
let rs = Self::rs_pubkey_from_dir_endpoint(&self.endpoint)?;
let url = self.endpoint.clone();
let body = serialize_v2_body(
Expand All @@ -314,7 +309,7 @@ impl RequestContext {
let body = crate::v2::encrypt_message_a(body, self.e, rs)
.map_err(InternalCreateRequestError::Hpke)?;
let (body, ohttp_res) = crate::v2::ohttp_encapsulate(
self.ohttp_keys.as_mut().ok_or(InternalCreateRequestError::MissingOhttpConfig)?,
self.endpoint.ohttp().as_mut().ok_or(InternalCreateRequestError::MissingOhttpConfig)?,
"POST",
url.as_str(),
Some(&body),
Expand Down Expand Up @@ -342,33 +337,22 @@ impl RequestContext {

#[cfg(feature = "v2")]
fn rs_pubkey_from_dir_endpoint(endpoint: &Url) -> Result<PublicKey, CreateRequestError> {
let path_and_query: String;

if let Some(pos) = endpoint.as_str().rfind('/') {
path_and_query = endpoint.as_str()[pos + 1..].to_string();
} else {
path_and_query = endpoint.to_string();
}

let subdirectory: String;

if let Some(pos) = path_and_query.find('?') {
subdirectory = path_and_query[..pos].to_string();
} else {
subdirectory = path_and_query;
}

let pubkey_bytes =
bitcoin::base64::decode_config(subdirectory, bitcoin::base64::URL_SAFE_NO_PAD)
.map_err(InternalCreateRequestError::SubdirectoryNotBase64)?;
Ok(bitcoin::secp256k1::PublicKey::from_slice(&pubkey_bytes)
.map_err(InternalCreateRequestError::SubdirectoryInvalidPubkey)?)
}

#[cfg(feature = "v2")]
pub fn public_key(&self) -> PublicKey {
let secp = bitcoin::secp256k1::Secp256k1::new();
self.e.public_key(&secp)
use bitcoin::base64;

use crate::send::error::ParseSubdirectoryError;

let subdirectory = endpoint
.path_segments()
.ok_or(ParseSubdirectoryError::MissingSubdirectory)?
.next()
.ok_or(ParseSubdirectoryError::MissingSubdirectory)?
.to_string();

let pubkey_bytes = base64::decode_config(subdirectory, base64::URL_SAFE_NO_PAD)
.map_err(ParseSubdirectoryError::SubdirectoryNotBase64)?;
bitcoin::secp256k1::PublicKey::from_slice(&pubkey_bytes)
.map_err(ParseSubdirectoryError::SubdirectoryInvalidPubkey)
.map_err(CreateRequestError::from)
}

pub fn endpoint(&self) -> &Url { &self.endpoint }
Expand All @@ -383,7 +367,6 @@ impl Serialize for RequestContext {
let mut state = serializer.serialize_struct("RequestContext", 8)?;
state.serialize_field("psbt", &self.psbt.to_string())?;
state.serialize_field("endpoint", &self.endpoint.as_str())?;
state.serialize_field("ohttp_keys", &self.ohttp_keys)?;
state.serialize_field("disable_output_substitution", &self.disable_output_substitution)?;
state.serialize_field(
"fee_contribution",
Expand Down Expand Up @@ -432,7 +415,6 @@ impl<'de> Deserialize<'de> for RequestContext {
{
let mut psbt = None;
let mut endpoint = None;
let mut ohttp_keys = None;
let mut disable_output_substitution = None;
let mut fee_contribution = None;
let mut min_fee_rate = None;
Expand All @@ -452,7 +434,6 @@ impl<'de> Deserialize<'de> for RequestContext {
url::Url::from_str(&map.next_value::<String>()?)
.map_err(de::Error::custom)?,
),
"ohttp_keys" => ohttp_keys = Some(map.next_value()?),
"disable_output_substitution" =>
disable_output_substitution = Some(map.next_value()?),
"fee_contribution" => {
Expand All @@ -478,7 +459,6 @@ impl<'de> Deserialize<'de> for RequestContext {
Ok(RequestContext {
psbt: psbt.ok_or_else(|| de::Error::missing_field("psbt"))?,
endpoint: endpoint.ok_or_else(|| de::Error::missing_field("endpoint"))?,
ohttp_keys: ohttp_keys.ok_or_else(|| de::Error::missing_field("ohttp_keys"))?,
disable_output_substitution: disable_output_substitution
.ok_or_else(|| de::Error::missing_field("disable_output_substitution"))?,
fee_contribution,
Expand Down Expand Up @@ -974,7 +954,7 @@ fn serialize_v2_body(
) -> Result<Vec<u8>, CreateRequestError> {
// Grug say localhost base be discarded anyway. no big brain needed.
let placeholder_url = serialize_url(
"http:/localhost".to_string(),
Url::parse("http://localhost").unwrap(),
disable_output_substitution,
fee_contribution,
min_feerate,
Expand All @@ -986,12 +966,12 @@ fn serialize_v2_body(
}

fn serialize_url(
endpoint: String,
endpoint: Url,
disable_output_substitution: bool,
fee_contribution: Option<(bitcoin::Amount, usize)>,
min_fee_rate: FeeRate,
) -> Result<Url, url::ParseError> {
let mut url = Url::parse(&endpoint)?;
let mut url = endpoint;
url.query_pairs_mut().append_pair("v", "1");
if disable_output_substitution {
url.query_pairs_mut().append_pair("disableoutputsubstitution", "1");
Expand Down Expand Up @@ -1065,7 +1045,6 @@ mod test {
let req_ctx = RequestContext {
psbt: Psbt::from_str(ORIGINAL_PSBT).unwrap(),
endpoint: Url::parse("http://localhost:1234").unwrap(),
ohttp_keys: None,
disable_output_substitution: false,
fee_contribution: None,
min_fee_rate: FeeRate::ZERO,
Expand Down
7 changes: 0 additions & 7 deletions payjoin/src/uri/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
#[cfg(feature = "v2")]
use crate::v2::ParseOhttpKeysError;

#[derive(Debug)]
pub struct PjParseError(InternalPjParseError);

Expand All @@ -11,8 +8,6 @@ pub(crate) enum InternalPjParseError {
MissingEndpoint,
NotUtf8,
BadEndpoint,
#[cfg(feature = "v2")]
BadOhttpKeys(ParseOhttpKeysError),
UnsecureEndpoint,
}

Expand All @@ -30,8 +25,6 @@ impl std::fmt::Display for PjParseError {
InternalPjParseError::MissingEndpoint => write!(f, "Missing payjoin endpoint"),
InternalPjParseError::NotUtf8 => write!(f, "Endpoint is not valid UTF-8"),
InternalPjParseError::BadEndpoint => write!(f, "Endpoint is not valid"),
#[cfg(feature = "v2")]
InternalPjParseError::BadOhttpKeys(e) => write!(f, "OHTTP keys are not valid: {}", e),
InternalPjParseError::UnsecureEndpoint => {
write!(f, "Endpoint scheme is not secure (https or onion)")
}
Expand Down
Loading

0 comments on commit 9dbdd97

Please sign in to comment.