Skip to content

Commit

Permalink
WebSocket proxy OHTTP KeyConfig fetch
Browse files Browse the repository at this point in the history
Bootstrap Oblivious HTTP without revealing a client IP to the directory.
  • Loading branch information
DanGould committed Apr 2, 2024
1 parent 7880f9d commit 44004c2
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 12 deletions.
53 changes: 51 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion mutiny-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ aes = { version = "0.8" }
jwt-compact = { version = "0.8.0-beta.1", features = ["es256k"] }
argon2 = { version = "0.5.0", features = ["password-hash", "alloc"] }
once_cell = "1.18.0"
gloo-net = { version = "0.5.0", features = ["io-util"] }
payjoin = { version = "0.15.0", features = ["v2", "send", "receive", "base64"] }
futures-rustls = { version = "0.25.1" }
rustls-pki-types = { version = "1.4.0", features = ["web"] }
webpki-roots = "0.26.1"
bincode = "1.3.3"
hex-conservative = "0.1.1"
async-lock = "3.2.0"
Expand Down Expand Up @@ -81,7 +85,6 @@ ignored_tests = []
wasm-bindgen-futures = { version = "0.4.38" }
web-sys = { version = "0.3.65", features = ["console"] }
js-sys = "0.3.65"
gloo-net = { version = "0.4.0" }
instant = { version = "0.1", features = ["wasm-bindgen"] }
getrandom = { version = "0.2", features = ["js"] }
# add nip07 feature for wasm32
Expand Down
2 changes: 2 additions & 0 deletions mutiny-core/src/nodemanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,8 @@ impl<S: MutinyStorage> NodeManager<S> {
pub async fn start_payjoin_session(&self) -> Result<(Enrolled, OhttpKeys), PayjoinError> {
use crate::payjoin::{OHTTP_RELAYS, PAYJOIN_DIR};

log_info!(self.logger, "Starting payjoin session");

let ohttp_keys =
crate::payjoin::fetch_ohttp_keys(OHTTP_RELAYS[0].to_owned(), PAYJOIN_DIR.to_owned())
.await?;
Expand Down
83 changes: 74 additions & 9 deletions mutiny-core/src/payjoin.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use std::collections::HashMap;
use std::sync::Arc;

use crate::error::MutinyError;
use crate::storage::MutinyStorage;
use core::time::Duration;
use gloo_net::websocket::futures::WebSocket;
use hex_conservative::DisplayHex;
use once_cell::sync::Lazy;
use payjoin::receive::v2::Enrolled;
Expand Down Expand Up @@ -69,16 +71,73 @@ impl<S: MutinyStorage> PayjoinStorage for S {
}
}

pub async fn fetch_ohttp_keys(_ohttp_relay: Url, directory: Url) -> Result<OhttpKeys, Error> {
let http_client = reqwest::Client::builder().build()?;
pub async fn fetch_ohttp_keys(ohttp_relay: Url, directory: Url) -> Result<OhttpKeys, Error> {
use futures_util::{AsyncReadExt, AsyncWriteExt};

let ohttp_keys_res = http_client
.get(format!("{}/ohttp-keys", directory.as_ref()))
.send()
.await?
.bytes()
.await?;
Ok(OhttpKeys::decode(ohttp_keys_res.as_ref()).map_err(|_| Error::OhttpDecodeFailed)?)
let tls_connector = {
let root_store = futures_rustls::rustls::RootCertStore {
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
};
let config = futures_rustls::rustls::ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
futures_rustls::TlsConnector::from(Arc::new(config))
};
let directory_host = directory.host_str().ok_or(Error::BadDirectoryHost)?;
let domain = futures_rustls::rustls::pki_types::ServerName::try_from(directory_host)
.map_err(|_| Error::BadDirectoryHost)?
.to_owned();

let ws = WebSocket::open(&format!(
"wss://{}:443",
ohttp_relay.host_str().ok_or(Error::BadOhttpWsHost)?
))
.map_err(|_| Error::BadOhttpWsHost)?;

let mut tls_stream = tls_connector
.connect(domain, ws)
.await
.map_err(|e| Error::RequestFailed(e.to_string()))?;
let ohttp_keys_req = format!(
"GET /ohttp-keys HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
directory_host
);
tls_stream
.write_all(ohttp_keys_req.as_bytes())
.await
.map_err(|e| Error::RequestFailed(e.to_string()))?;
tls_stream
.flush()
.await
.map_err(|e| Error::RequestFailed(e.to_string()))?;
let mut response_bytes = Vec::new();
tls_stream
.read_to_end(&mut response_bytes)
.await
.map_err(|e| Error::RequestFailed(e.to_string()))?;
let (_headers, res_body) = separate_headers_and_body(&response_bytes)?;
payjoin::OhttpKeys::decode(res_body).map_err(|_| Error::OhttpDecodeFailed)
}

fn separate_headers_and_body(response_bytes: &[u8]) -> Result<(&[u8], &[u8]), Error> {
let separator = b"\r\n\r\n";

// Search for the separator
if let Some(position) = response_bytes
.windows(separator.len())
.position(|window| window == separator)
{
// The body starts immediately after the separator
let body_start_index = position + separator.len();
let headers = &response_bytes[..position];
let body = &response_bytes[body_start_index..];

Ok((headers, body))
} else {
Err(Error::RequestFailed(
"No header-body separator found in the response".to_string(),
))
}
}

#[derive(Debug)]
Expand All @@ -89,6 +148,9 @@ pub enum Error {
OhttpDecodeFailed,
Shutdown,
SessionExpired,
BadDirectoryHost,
BadOhttpWsHost,
RequestFailed(String),
}

impl std::error::Error for Error {}
Expand All @@ -102,6 +164,9 @@ impl std::fmt::Display for Error {
Error::OhttpDecodeFailed => write!(f, "Failed to decode ohttp keys"),
Error::Shutdown => write!(f, "Payjoin stopped by application shutdown"),
Error::SessionExpired => write!(f, "Payjoin session expired. Create a new payment request and have the sender try again."),
Error::BadDirectoryHost => write!(f, "Bad directory host"),
Error::BadOhttpWsHost => write!(f, "Bad ohttp ws host"),
Error::RequestFailed(e) => write!(f, "Request failed: {}", e),
}
}
}
Expand Down

0 comments on commit 44004c2

Please sign in to comment.