Skip to content
This repository was archived by the owner on Feb 3, 2025. It is now read-only.

Commit 4573358

Browse files
committed
WebSocket proxy OHTTP KeyConfig fetch
Bootstrap Oblivious HTTP without revealing a client IP to the directory.
1 parent e84d435 commit 4573358

4 files changed

Lines changed: 98 additions & 10 deletions

File tree

Cargo.lock

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mutiny-core/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ aes = { version = "0.8" }
4545
jwt-compact = { version = "0.8.0-beta.1", features = ["es256k"] }
4646
argon2 = { version = "0.5.0", features = ["password-hash", "alloc"] }
4747
once_cell = "1.18.0"
48+
gloo-net = { version = "0.5.0", features = ["io-util"] }
4849
payjoin = { version = "0.15.0", features = ["v2", "send", "receive", "base64"] }
50+
futures-rustls = { version = "0.25.1" }
51+
rustls-pki-types = { version = "1.4.0", features = ["web"] }
52+
webpki-roots = "0.26.1"
4953
bincode = "1.3.3"
5054
hex-conservative = "0.1.1"
5155
async-lock = "3.2.0"
@@ -85,6 +89,8 @@ wasm-bindgen-futures = { version = "0.4.38" }
8589
gloo-net = { version = "0.4.0" }
8690
web-time = "1.1"
8791
gloo-timers = { version = "0.3.0", features = ["futures"] }
92+
web-sys = { version = "0.3.65", features = ["console"] }
93+
js-sys = "0.3.65"
8894
getrandom = { version = "0.2", features = ["js"] }
8995
# add nip07 feature for wasm32
9096
nostr = { version = "0.29.0", default-features = false, features = ["nip04", "nip05", "nip07", "nip47", "nip57"] }

mutiny-core/src/nodemanager.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -680,7 +680,9 @@ impl<S: MutinyStorage> NodeManager<S> {
680680
) -> Result<(Enrolled, payjoin::OhttpKeys), PayjoinError> {
681681
use crate::payjoin::{fetch_ohttp_keys, random_ohttp_relay, PAYJOIN_DIR};
682682

683-
let ohttp_keys = fetch_ohttp_keys(PAYJOIN_DIR.to_owned()).await?;
683+
log_info!(self.logger, "Starting payjoin session");
684+
685+
let ohttp_keys = fetch_ohttp_keys().await?;
684686
let http_client = reqwest::Client::builder().build()?;
685687

686688
let mut enroller = payjoin::receive::v2::Enroller::from_directory_config(

mutiny-core/src/payjoin.rs

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use std::collections::HashMap;
2+
use std::sync::Arc;
23

34
use crate::error::MutinyError;
45
use crate::storage::MutinyStorage;
56
use bitcoin::Transaction;
67
use core::time::Duration;
8+
use gloo_net::websocket::futures::WebSocket;
79
use hex_conservative::DisplayHex;
810
use once_cell::sync::Lazy;
911
use payjoin::receive::v2::Enrolled;
@@ -77,16 +79,75 @@ impl<S: MutinyStorage> PayjoinStorage for S {
7779
}
7880
}
7981

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

83-
let ohttp_keys_res = http_client
84-
.get(format!("{}/ohttp-keys", directory.as_ref()))
85-
.send()
86-
.await?
87-
.bytes()
88-
.await?;
89-
OhttpKeys::decode(ohttp_keys_res.as_ref()).map_err(|_| Error::OhttpDecodeFailed)
85+
let tls_connector = {
86+
let root_store = futures_rustls::rustls::RootCertStore {
87+
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
88+
};
89+
let config = futures_rustls::rustls::ClientConfig::builder()
90+
.with_root_certificates(root_store)
91+
.with_no_client_auth();
92+
futures_rustls::TlsConnector::from(Arc::new(config))
93+
};
94+
let directory_host = PAYJOIN_DIR.host_str().ok_or(Error::BadDirectoryHost)?;
95+
let domain = futures_rustls::rustls::pki_types::ServerName::try_from(directory_host)
96+
.map_err(|_| Error::BadDirectoryHost)?
97+
.to_owned();
98+
99+
let ws = WebSocket::open(&format!(
100+
"wss://{}:443",
101+
random_ohttp_relay()
102+
.host_str()
103+
.ok_or(Error::BadOhttpWsHost)?
104+
))
105+
.map_err(|_| Error::BadOhttpWsHost)?;
106+
107+
let mut tls_stream = tls_connector
108+
.connect(domain, ws)
109+
.await
110+
.map_err(|e| Error::RequestFailed(e.to_string()))?;
111+
let ohttp_keys_req = format!(
112+
"GET /ohttp-keys HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
113+
directory_host
114+
);
115+
tls_stream
116+
.write_all(ohttp_keys_req.as_bytes())
117+
.await
118+
.map_err(|e| Error::RequestFailed(e.to_string()))?;
119+
tls_stream
120+
.flush()
121+
.await
122+
.map_err(|e| Error::RequestFailed(e.to_string()))?;
123+
let mut response_bytes = Vec::new();
124+
tls_stream
125+
.read_to_end(&mut response_bytes)
126+
.await
127+
.map_err(|e| Error::RequestFailed(e.to_string()))?;
128+
let (_headers, res_body) = separate_headers_and_body(&response_bytes)?;
129+
payjoin::OhttpKeys::decode(res_body).map_err(|_| Error::OhttpDecodeFailed)
130+
}
131+
132+
fn separate_headers_and_body(response_bytes: &[u8]) -> Result<(&[u8], &[u8]), Error> {
133+
let separator = b"\r\n\r\n";
134+
135+
// Search for the separator
136+
if let Some(position) = response_bytes
137+
.windows(separator.len())
138+
.position(|window| window == separator)
139+
{
140+
// The body starts immediately after the separator
141+
let body_start_index = position + separator.len();
142+
let headers = &response_bytes[..position];
143+
let body = &response_bytes[body_start_index..];
144+
145+
Ok((headers, body))
146+
} else {
147+
Err(Error::RequestFailed(
148+
"No header-body separator found in the response".to_string(),
149+
))
150+
}
90151
}
91152

92153
#[derive(Debug)]

0 commit comments

Comments
 (0)