Skip to content

Commit

Permalink
Protect metadata with Oblivious HTTP
Browse files Browse the repository at this point in the history
  • Loading branch information
DanGould committed Nov 6, 2023
1 parent 86a64e6 commit 5243442
Show file tree
Hide file tree
Showing 15 changed files with 1,012 additions and 327 deletions.
426 changes: 388 additions & 38 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[workspace]
resolver = "2"
members = ["payjoin", "payjoin-cli", "payjoin-relay"]

134 changes: 77 additions & 57 deletions payjoin-cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ use clap::ArgMatches;
use config::{Config, File, FileFormat};
use payjoin::bitcoin::psbt::Psbt;
use payjoin::bitcoin::{self, base64};
use payjoin::receive::{Error, PayjoinProposal, ProvisionalProposal, UncheckedProposal};
use payjoin::receive::{
EnrollContext, Error, PayjoinProposal, ProvisionalProposal, UncheckedProposal,
};
use payjoin::send::RequestContext;
#[cfg(not(feature = "v2"))]
use rouille::{Request, Response};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -44,64 +47,61 @@ impl App {

#[cfg(feature = "v2")]
pub fn send_payjoin(&self, bip21: &str, fee_rate: &f32) -> Result<()> {
let (req, ctx) = self.create_pj_request(bip21, fee_rate)?;
let req_ctx = self.create_pj_request(bip21, fee_rate)?;

let client = reqwest::blocking::Client::builder()
.danger_accept_invalid_certs(self.config.danger_accept_invalid_certs)
.build()
.with_context(|| "Failed to build reqwest http client")?;

log::debug!("Awaiting response");
let res = Self::long_poll_post(&client, req)?;
let mut res = std::io::Cursor::new(&res);
self.process_pj_response(ctx, &mut res)?;
let res = self.long_poll_post(&client, req_ctx)?;
self.process_pj_response(res)?;
Ok(())
}

#[cfg(feature = "v2")]
fn long_poll_post(
&self,
client: &reqwest::blocking::Client,
req: payjoin::send::Request,
) -> Result<Vec<u8>, reqwest::Error> {
req_ctx: payjoin::send::RequestContext<'_>,
) -> Result<Psbt> {
loop {
let (req, ctx) = req_ctx.extract_v2(&self.config.ohttp_proxy)?;
let response = client
.post(req.url.as_str())
.post(req.url)
.body(req.body.clone())
.header("Content-Type", "text/plain")
.header("Async", "true")
.send()?;

if response.status() == reqwest::StatusCode::OK {
let body = response.bytes()?.to_vec();
return Ok(body);
} else if response.status() == reqwest::StatusCode::ACCEPTED {
let bytes = response.bytes()?;
let mut cursor = std::io::Cursor::new(bytes);
let psbt = ctx.process_response(&mut cursor)?;
if let Some(psbt) = psbt {
return Ok(psbt);
} else {
log::info!("No response yet for POST payjoin request, retrying some seconds");
std::thread::sleep(std::time::Duration::from_secs(5));
} else {
log::error!("Unexpected response status: {}", response.status());
// TODO handle error
panic!("Unexpected response status: {}", response.status())
}
}
}

#[cfg(feature = "v2")]
fn long_poll_get(
&self,
client: &reqwest::blocking::Client,
url: &str,
) -> Result<reqwest::blocking::Response, reqwest::Error> {
enroll_context: &mut EnrollContext,
) -> Result<UncheckedProposal, reqwest::Error> {
loop {
let response = client.get(url).send()?;

if response.status() == reqwest::StatusCode::OK {
return Ok(response);
} else if response.status() == reqwest::StatusCode::ACCEPTED {
log::info!("No response yet for GET payjoin request, retrying in 5 seconds");
std::thread::sleep(std::time::Duration::from_secs(5));
} else {
log::error!("Unexpected response status: {}", response.status());
// TODO handle error
panic!("Unexpected response status: {}", response.status())
let (payjoin_get_body, context) = enroll_context.payjoin_get_body();
let ohttp_response =
client.post(&self.config.ohttp_proxy).body(payjoin_get_body).send()?;
let ohttp_response = ohttp_response.bytes()?;
let proposal =
enroll_context.parse_relay_response(ohttp_response.as_ref(), context).unwrap();
match proposal {
Some(proposal) => return Ok(proposal),
None => std::thread::sleep(std::time::Duration::from_secs(5)),
}
}
}
Expand All @@ -125,11 +125,7 @@ impl App {
Ok(())
}

fn create_pj_request(
&self,
bip21: &str,
fee_rate: &f32,
) -> Result<(payjoin::send::Request, payjoin::send::Context)> {
fn create_pj_request<'a>(&self, bip21: &'a str, fee_rate: &f32) -> Result<RequestContext<'a>> {
let uri = payjoin::Uri::try_from(bip21)
.map_err(|e| anyhow!("Failed to create URI from BIP21: {}", e))?;

Expand Down Expand Up @@ -170,22 +166,16 @@ impl App {
.psbt;
let psbt = Psbt::from_str(&psbt).with_context(|| "Failed to load PSBT from base64")?;
log::debug!("Original psbt: {:#?}", psbt);

let (req, ctx) = payjoin::send::RequestBuilder::from_psbt_and_uri(psbt, uri)
let req_ctx = payjoin::send::RequestBuilder::from_psbt_and_uri(psbt, uri)
.with_context(|| "Failed to build payjoin request")?
.build_recommended(fee_rate)
.with_context(|| "Failed to build payjoin request")?;

Ok((req, ctx))
Ok(req_ctx)
}

fn process_pj_response(
&self,
ctx: payjoin::send::Context,
response: &mut impl std::io::Read,
) -> Result<bitcoin::Txid> {
fn process_pj_response(&self, psbt: Psbt) -> Result<bitcoin::Txid> {
// TODO display well-known errors and log::debug the rest
let psbt = ctx.process_response(response).with_context(|| "Failed to process response")?;
log::debug!("Proposed psbt: {:#?}", psbt);
let psbt = self
.bitcoind
Expand Down Expand Up @@ -221,7 +211,11 @@ impl App {

#[cfg(feature = "v2")]
pub fn receive_payjoin(self, amount_arg: &str) -> Result<()> {
let context = payjoin::receive::ProposalContext::new();
let mut context = EnrollContext::from_relay_config(
&self.config.pj_endpoint,
&self.config.ohttp_config,
&self.config.ohttp_proxy,
);
let pj_uri_string =
self.construct_payjoin_uri(amount_arg, Some(&context.subdirectory()))?;
println!(
Expand All @@ -235,25 +229,25 @@ impl App {
.build()
.with_context(|| "Failed to build reqwest http client")?;
log::debug!("Awaiting request");
let _enroll = client.post(&self.config.pj_endpoint).body(pubkey_base64.clone()).send()?;

let receive_endpoint = format!("{}/{}", self.config.pj_endpoint, context.receive_subdir());
let res = Self::long_poll_get(&client, &receive_endpoint)?;
let _enroll = client.post(&self.config.pj_endpoint).body(context.enroll_body()).send()?;

log::debug!("Awaiting proposal");
let res = self.long_poll_get(&client, &mut context)?;
log::debug!("Received request");
let proposal = context
.parse_relay_response(res)
.map_err(|e| anyhow!("Failed to parse into UncheckedProposal {}", e))?;
let payjoin_proposal = self
.process_proposal(proposal)
.map_err(|e| anyhow!("Failed to process UncheckedProposal {}", e))?;
let payjoin_endpoint = format!("{}/{}/receive", self.config.pj_endpoint, pubkey_base64);
let body = payjoin_proposal.serialize_body();
let _ = client
.post(payjoin_endpoint)
let (body, ohttp_ctx) =
payjoin_proposal.extract_v2_req(&self.config.ohttp_config, &payjoin_endpoint);
let res = client
.post(&self.config.ohttp_proxy)
.body(body)
.send()
.with_context(|| "HTTP request failed")?;
let res = res.bytes()?;
let res = payjoin_proposal.deserialize_res(res.to_vec(), ohttp_ctx);
log::debug!("Received response {:?}", res);
Ok(())
}

Expand Down Expand Up @@ -289,14 +283,15 @@ impl App {
let pj_receiver_address = self.bitcoind.get_new_address(None, None)?.assume_checked();
let amount = Amount::from_sat(amount_arg.parse()?);
let pj_uri_string = format!(
"{}?amount={}&pj={}",
"{}?amount={}&pj={}&ohttp={}",
pj_receiver_address.to_qr_uri(),
amount.to_btc(),
format!(
"{}{}",
self.config.pj_endpoint,
pubkey.map_or(String::from(""), |s| format!("/{}", s))
)
),
self.config.ohttp_config,
);

// to check uri validity
Expand Down Expand Up @@ -475,6 +470,19 @@ impl App {
}
}

fn serialize_request_to_bytes(req: reqwest::Request) -> Vec<u8> {
let mut serialized_request =
format!("{} {} HTTP/1.1\r\n", req.method(), req.url()).into_bytes();

for (name, value) in req.headers().iter() {
let header_line = format!("{}: {}\r\n", name.as_str(), value.to_str().unwrap());
serialized_request.extend(header_line.as_bytes());
}

serialized_request.extend(b"\r\n");
serialized_request
}

struct SeenInputs {
set: OutPointSet,
file: std::fs::File,
Expand Down Expand Up @@ -514,6 +522,8 @@ pub(crate) struct AppConfig {
pub bitcoind_cookie: Option<String>,
pub bitcoind_rpcuser: String,
pub bitcoind_rpcpass: String,
pub ohttp_config: String,
pub ohttp_proxy: String,

// send-only
pub danger_accept_invalid_certs: bool,
Expand Down Expand Up @@ -547,6 +557,16 @@ impl AppConfig {
"bitcoind_rpcpass",
matches.get_one::<String>("rpcpass").map(|s| s.as_str()),
)?
.set_default("ohttp_config", "")?
.set_override_option(
"ohttp_config",
matches.get_one::<String>("ohttp_config").map(|s| s.as_str()),
)?
.set_default("ohttp_proxy", "")?
.set_override_option(
"ohttp_proxy",
matches.get_one::<String>("ohttp_proxy").map(|s| s.as_str()),
)?
// Subcommand defaults without which file serialization fails.
.set_default("danger_accept_invalid_certs", false)?
.set_default("pj_host", "0.0.0.0:3000")?
Expand Down
6 changes: 6 additions & 0 deletions payjoin-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ fn cli() -> ArgMatches {
.long("rpcpass")
.help("The password for the bitcoin node"))
.subcommand_required(true)
.arg(Arg::new("ohttp_config")
.long("ohttp-config")
.help("The ohttp config file"))
.arg(Arg::new("ohttp_proxy")
.long("ohttp-proxy")
.help("The ohttp proxy url"))
.subcommand(
Command::new("send")
.arg_required_else_help(true)
Expand Down
4 changes: 3 additions & 1 deletion payjoin-relay/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ edition = "2021"
[dependencies]
hyper = { version = "0.14", features = ["full"] }
anyhow = "1.0.71"
payjoin = { path = "../payjoin", features = ["base64"] }
payjoin = { path = "../payjoin", features = ["base64", "v2"] }
ohttp = "0.4.0"
bhttp = { version = "0.4.0", features = ["http"] }
sqlx = { version = "0.7.1", features = ["postgres", "runtime-tokio"] }
tokio = { version = "1.12.0", features = ["full"] }
tracing = "0.1.37"
Expand Down
Loading

0 comments on commit 5243442

Please sign in to comment.