diff --git a/Cargo.lock b/Cargo.lock index 9833e0313..a854f0d04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1636,6 +1636,17 @@ dependencies = [ "syn 2.0.55", ] +[[package]] +name = "futures-rustls" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d8a2499f0fecc0492eb3e47eab4e92da7875e1028ad2528f214ac3346ca04e" +dependencies = [ + "futures-io", + "rustls 0.22.3", + "rustls-pki-types", +] + [[package]] name = "futures-sink" version = "0.3.30" @@ -1754,6 +1765,7 @@ checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" dependencies = [ "futures-channel", "futures-core", + "futures-io", "futures-sink", "gloo-utils", "http 0.2.12", @@ -2760,9 +2772,11 @@ dependencies = [ "fedimint-tbs", "fedimint-wallet-client", "futures", + "futures-rustls", "futures-util", "getrandom", "gloo-net 0.4.0", + "gloo-net 0.5.0", "gloo-timers 0.3.0", "hex-conservative", "itertools 0.11.0", @@ -2785,6 +2799,7 @@ dependencies = [ "payjoin", "pbkdf2 0.11.0", "reqwest", + "rustls-pki-types", "serde", "serde_json", "thiserror", @@ -2796,6 +2811,7 @@ dependencies = [ "wasm-bindgen-test", "web-sys", "web-time", + "webpki-roots 0.26.1", ] [[package]] @@ -3725,6 +3741,9 @@ name = "rustls-pki-types" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "868e20fada228fefaf6b652e00cc73623d54f8171e7352c18bb281571f2d92da" +dependencies = [ + "web-time", +] [[package]] name = "rustls-webpki" diff --git a/mutiny-core/Cargo.toml b/mutiny-core/Cargo.toml index e522e4754..2409d8118 100644 --- a/mutiny-core/Cargo.toml +++ b/mutiny-core/Cargo.toml @@ -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" @@ -85,6 +89,8 @@ wasm-bindgen-futures = { version = "0.4.38" } gloo-net = { version = "0.4.0" } web-time = "1.1" gloo-timers = { version = "0.3.0", features = ["futures"] } +web-sys = { version = "0.3.65", features = ["console"] } +js-sys = "0.3.65" getrandom = { version = "0.2", features = ["js"] } # add nip07 feature for wasm32 nostr = { version = "0.29.0", default-features = false, features = ["nip04", "nip05", "nip07", "nip47", "nip57"] } diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 04eeab243..a2060f421 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -683,7 +683,9 @@ impl NodeManager { ) -> Result<(Enrolled, payjoin::OhttpKeys), PayjoinError> { use crate::payjoin::{fetch_ohttp_keys, random_ohttp_relay, PAYJOIN_DIR}; - let ohttp_keys = fetch_ohttp_keys(PAYJOIN_DIR.to_owned()).await?; + log_info!(self.logger, "Starting payjoin session"); + + let ohttp_keys = fetch_ohttp_keys().await?; let http_client = reqwest::Client::builder().build()?; let mut enroller = payjoin::receive::v2::Enroller::from_directory_config( diff --git a/mutiny-core/src/payjoin.rs b/mutiny-core/src/payjoin.rs index d1f9909f6..8caa7d218 100644 --- a/mutiny-core/src/payjoin.rs +++ b/mutiny-core/src/payjoin.rs @@ -1,9 +1,11 @@ use std::collections::HashMap; +use std::sync::Arc; use crate::error::MutinyError; use crate::storage::MutinyStorage; use bitcoin::Transaction; use core::time::Duration; +use gloo_net::websocket::futures::WebSocket; use hex_conservative::DisplayHex; use once_cell::sync::Lazy; use payjoin::receive::v2::Enrolled; @@ -77,16 +79,75 @@ impl PayjoinStorage for S { } } -pub async fn fetch_ohttp_keys(directory: Url) -> Result { - let http_client = reqwest::Client::builder().build()?; +pub async fn fetch_ohttp_keys() -> Result { + use futures_util::{AsyncReadExt, AsyncWriteExt}; - let ohttp_keys_res = http_client - .get(format!("{}/ohttp-keys", directory.as_ref())) - .send() - .await? - .bytes() - .await?; - 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 = PAYJOIN_DIR.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", + random_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)]