diff --git a/Cargo.lock b/Cargo.lock index 14384cbd8..2b55971d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1616,6 +1616,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" @@ -1726,6 +1737,28 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-net" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "gloo-utils", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "gloo-timers" version = "0.2.6" @@ -2232,7 +2265,7 @@ checksum = "b5b005c793122d03217da09af68ba9383363caa950b90d3436106df8cabce935" dependencies = [ "futures-channel", "futures-util", - "gloo-net", + "gloo-net 0.4.0", "http 0.2.12", "jsonrpsee-core", "pin-project", @@ -2698,9 +2731,10 @@ dependencies = [ "fedimint-tbs", "fedimint-wallet-client", "futures", + "futures-rustls", "futures-util", "getrandom", - "gloo-net", + "gloo-net 0.5.0", "hex-conservative", "instant", "itertools 0.11.0", @@ -2723,6 +2757,7 @@ dependencies = [ "payjoin", "pbkdf2 0.11.0", "reqwest", + "rustls-pki-types", "serde", "serde_json", "thiserror", @@ -2733,6 +2768,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-bindgen-test", "web-sys", + "webpki-roots 0.26.1", ] [[package]] @@ -3653,6 +3689,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" @@ -4864,6 +4903,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki" version = "0.22.4" diff --git a/mutiny-core/Cargo.toml b/mutiny-core/Cargo.toml index b4e00e9ce..f7982989a 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" @@ -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 diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 23e2fb660..e58dd2436 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -730,6 +730,8 @@ impl NodeManager { 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?; diff --git a/mutiny-core/src/payjoin.rs b/mutiny-core/src/payjoin.rs index 6171be002..d4d2de175 100644 --- a/mutiny-core/src/payjoin.rs +++ b/mutiny-core/src/payjoin.rs @@ -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; @@ -69,16 +71,73 @@ impl PayjoinStorage for S { } } -pub async fn fetch_ohttp_keys(_ohttp_relay: Url, directory: Url) -> Result { - let http_client = reqwest::Client::builder().build()?; +pub async fn fetch_ohttp_keys(ohttp_relay: Url, directory: Url) -> Result { + 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)] @@ -89,6 +148,9 @@ pub enum Error { OhttpDecodeFailed, Shutdown, SessionExpired, + BadDirectoryHost, + BadOhttpWsHost, + RequestFailed(String), } impl std::error::Error for Error {} @@ -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), } } }