diff --git a/mm2src/adex_cli/Cargo.lock b/mm2src/adex_cli/Cargo.lock index 37367f7ed8..26fd951793 100644 --- a/mm2src/adex_cli/Cargo.lock +++ b/mm2src/adex_cli/Cargo.lock @@ -24,6 +24,8 @@ dependencies = [ "env_logger 0.7.1", "gstuff", "http 0.2.9", + "hyper", + "hyper-rustls", "inquire", "itertools", "log 0.4.17", @@ -32,6 +34,7 @@ dependencies = [ "mm2_rpc", "passwords", "rpc", + "rustls 0.20.8", "serde", "serde_json", "sysinfo", @@ -560,6 +563,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -1328,7 +1341,9 @@ checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" dependencies = [ "http 0.2.9", "hyper", + "log 0.4.17", "rustls 0.20.8", + "rustls-native-certs", "tokio", "tokio-rustls", "webpki-roots", @@ -1944,6 +1959,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + [[package]] name = "option-ext" version = "0.2.0" @@ -2637,17 +2658,48 @@ version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" dependencies = [ + "log 0.4.17", "ring", "sct 0.7.0", "webpki 0.22.0", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.2", +] + [[package]] name = "ryu" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -2738,6 +2790,29 @@ dependencies = [ "cc", ] +[[package]] +name = "security-framework" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "0.9.0" diff --git a/mm2src/adex_cli/Cargo.toml b/mm2src/adex_cli/Cargo.toml index 9af19a3ab0..ec0e0e5283 100644 --- a/mm2src/adex_cli/Cargo.toml +++ b/mm2src/adex_cli/Cargo.toml @@ -15,6 +15,8 @@ derive_more = "0.99" directories = "5.0" env_logger = "0.7.1" http = "0.2" +hyper = { version = "0.14.26", features = ["client", "http2", "tcp"] } +hyper-rustls = "^0.23.0" gstuff = { version = "=0.7.4" , features = [ "nightly" ]} inquire = "0.6" itertools = "0.10" @@ -23,14 +25,14 @@ mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number" } mm2_rpc = { path = "../mm2_rpc"} passwords = "3.1" +rpc = { path = "../mm2_bitcoin/rpc" } +rustls = { version = "^0.20.4", features = [ "dangerous_configuration" ] } serde = "1.0" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } sysinfo = "0.28" tiny-bip39 = "0.8.0" tokio = { version = "1.20", features = [ "macros" ] } uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } -rpc = { path = "../mm2_bitcoin/rpc" } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.3", features = ["processthreadsapi", "winnt"] } - diff --git a/mm2src/adex_cli/src/transport.rs b/mm2src/adex_cli/src/transport.rs index f2726908fd..92e28c0655 100644 --- a/mm2src/adex_cli/src/transport.rs +++ b/mm2src/adex_cli/src/transport.rs @@ -1,10 +1,12 @@ use anyhow::{anyhow, bail, Result}; use async_trait::async_trait; use http::{HeaderMap, StatusCode}; -use log::{error, warn}; -use mm2_net::native_http::slurp_post_json; use serde::{Deserialize, Serialize}; +use common::log::{error, warn}; +use hyper_dangerous::get_hyper_client_dangerous; +use mm2_net::native_http::SlurpHttpClient; + use crate::{error_anyhow, error_bail, warn_bail}; #[async_trait] @@ -32,8 +34,10 @@ impl Transport for SlurpTransport { OkT: for<'a> Deserialize<'a>, ErrT: for<'a> Deserialize<'a>, { - let data = serde_json::to_string(&req).expect("Failed to serialize enable request"); - match slurp_post_json(&self.rpc_uri, data).await { + let data = serde_json::to_string(&req) + .map_err(|error| error_anyhow!("Failed to serialize data being sent: {error}"))?; + let client = get_hyper_client_dangerous()?; + match client.slurp_post_json(&self.rpc_uri, data).await { Err(error) => error_bail!("Failed to send json: {error}"), Ok(resp) => resp.process::(), } @@ -78,3 +82,54 @@ impl Response for (StatusCode, HeaderMap, Vec) { } } } + +mod hyper_dangerous { + + use hyper::{client::HttpConnector, Body, Client}; + use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; + use rustls::client::{ServerCertVerified, ServerCertVerifier}; + use rustls::{RootCertStore, DEFAULT_CIPHER_SUITES, DEFAULT_VERSIONS}; + use std::sync::Arc; + use std::time::SystemTime; + + use super::*; + + pub(super) fn get_hyper_client_dangerous() -> Result>> { + let mut config = rustls::ClientConfig::builder() + .with_cipher_suites(DEFAULT_CIPHER_SUITES) + .with_safe_default_kx_groups() + .with_protocol_versions(DEFAULT_VERSIONS) + .map_err(|error| error_anyhow!("Inconsistent cipher-suite/versions selected: {error}"))? + .with_root_certificates(RootCertStore::empty()) + .with_no_client_auth(); + + config + .dangerous() + .set_certificate_verifier(Arc::new(NoCertificateVerification {})); + + let https_connector = HttpsConnectorBuilder::default() + .with_tls_config(config) + .https_or_http() + .enable_http1() + .enable_http2() + .build(); + + Ok(Client::builder().build::<_, Body>(https_connector)) + } + + struct NoCertificateVerification {} + + impl ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _: &rustls::Certificate, + _: &[rustls::Certificate], + _: &rustls::ServerName, + _: &mut dyn Iterator, + _: &[u8], + _: SystemTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + } +} diff --git a/mm2src/mm2_net/src/native_http.rs b/mm2src/mm2_net/src/native_http.rs index 700bdb1efa..9a79611835 100644 --- a/mm2src/mm2_net/src/native_http.rs +++ b/mm2src/mm2_net/src/native_http.rs @@ -1,12 +1,212 @@ -use crate::transport::{SlurpError, SlurpResult, SlurpResultJson}; -use common::wio::{drive03, HYPER}; -use common::APPLICATION_JSON; +//! Facilitates execution of http requests +//! +//! # Layout +//! +//! This module contains several service functions like [slurp_post_json] or [slurp_url_with_headers] +//! for executing http protocol requests, allowing you to set certain headers, make a [`Request`] in JSON or [`Body`] format. +//! +//! These methods are wrappers over [`HYPER`], which is actually `Client>` that implements +//! [`SlurpHttpClient`] trait designed to provide http capabilities through it. +//! +//! There are also facilities for constructing [SlurpError] from the [hyper::Error] +//! + +use async_trait::async_trait; use futures::channel::oneshot::Canceled; use http::{header, HeaderValue, Request}; -use hyper::Body; -use mm2_err_handle::prelude::*; +use hyper::client::connect::Connect; +use hyper::client::ResponseFuture; +use hyper::{Body, Client}; use serde_json::Value as Json; +use common::wio::{drive03, HYPER}; +use common::APPLICATION_JSON; +use mm2_err_handle::prelude::*; + +use super::transport::{SlurpError, SlurpResult, SlurpResultJson}; + +/// Provides requesting http through it +/// +/// Initially designed to be used with [hyper::Client] that could be constructed in different specific ways. +/// one of which is using with statically defined [HYPER] that is common client able to request https or https urls +/// In the other case it can be a dangerous client that does not verify self signed signature +/// +/// # Examples +/// +/// Request over both http or https using common [hyper_rustls::HttpsConnectorBuilder] +/// +/// ```rust +/// let https = HttpsConnectorBuilder::new() +/// .with_webpki_roots() +/// .https_or_http() +/// .enable_http1() +/// .enable_http2() +/// .build(); +/// let client = Client::builder().pool_max_idle_per_host(0).build(https) +/// client.slurp_url(`https://komodoproject.com`) +/// ``` +/// +/// Request over https with self-signed certificate +/// +/// ```rust +/// let data = serde_json::to_string(&req).map_err(|error| error_anyhow!("Failed to serialize data being sent: {error}"))?; +/// match HYPER_DANGEROUS.slurp_post_json(&self.rpc_uri, data).await { +/// Err(error) => error_bail!("Failed to send json: {error}"), +/// Ok(resp) => resp.process::(), +/// } +/// +/// mod hyper_dangerous { +/// use hyper::{client::HttpConnector, Body, Client}; +/// use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; +/// use lazy_static::lazy_static; +/// use rustls::client::{ServerCertVerified, ServerCertVerifier}; +/// use rustls::{RootCertStore, DEFAULT_CIPHER_SUITES, DEFAULT_VERSIONS}; +/// use std::sync::Arc; +/// use std::time::SystemTime; +/// +/// lazy_static! { +/// pub(super) static ref HYPER_DANGEROUS: Client> = get_hyper_client_dangerous(); +/// } +/// +/// fn get_hyper_client_dangerous() -> Client> { +/// let mut config = rustls::ClientConfig::builder() +/// .with_cipher_suites(&DEFAULT_CIPHER_SUITES) +/// .with_safe_default_kx_groups() +/// .with_protocol_versions(&DEFAULT_VERSIONS) +/// .expect("inconsistent cipher-suite/versions selected") +/// .with_root_certificates(RootCertStore::empty()) +/// .with_no_client_auth(); +/// +/// config +/// .dangerous() +/// .set_certificate_verifier(Arc::new(NoCertificateVerification {})); +/// +/// let https_connector = HttpsConnectorBuilder::default() +/// .with_tls_config(config) +/// .https_or_http() +/// .enable_http1() +/// .build(); +/// +/// Client::builder().build::<_, Body>(https_connector) +/// } +/// +/// struct NoCertificateVerification {} +/// +/// impl ServerCertVerifier for NoCertificateVerification { +/// fn verify_server_cert( +/// &self, +/// _: &rustls::Certificate, +/// _: &[rustls::Certificate], +/// _: &rustls::ServerName, +/// _: &mut dyn Iterator, +/// _: &[u8], +/// _: SystemTime, +/// ) -> Result { +/// Ok(ServerCertVerified::assertion()) +/// } +/// } +/// } +/// ``` +#[async_trait] +pub trait SlurpHttpClient { + /// Provides a [ResponseFuture] that could be spawned and processed asynchronously + fn request(&self, req: Request) -> ResponseFuture; + + /// Executes a POST request, returning the response status, headers and body. + async fn slurp_post_json(&self, url: &str, body: String) -> SlurpResult { + let request = Request::builder() + .method("POST") + .uri(url) + .header(header::CONTENT_TYPE, APPLICATION_JSON) + .body(body.into())?; + self.slurp_req(request).await + } + + /// Executes a GET request, returning the response status, headers and body. + async fn slurp_url(&self, url: &str) -> SlurpResult { + let req = Request::builder().uri(url).body(Vec::new())?; + self.slurp_req(req).await + } + + /// Executes a GET request with additional headers. + /// Returning the response status, headers and body. + async fn slurp_url_with_headers(&self, url: &str, headers: Vec<(&'static str, &'static str)>) -> SlurpResult { + let mut req = Request::builder(); + let h = req + .headers_mut() + .or_mm_err(|| SlurpError::Internal("An error occurred when accessing the request headers".to_string()))?; + + for (key, value) in headers { + h.insert(key, HeaderValue::from_static(value)); + } + + let req = req.uri(url).body(Vec::new())?; + self.slurp_req(req).await + } + + /// Executes a Hyper request, requires [`Request`] and return the response status, headers and body as Json. + async fn slurp_req_body(&self, request: Request) -> SlurpResultJson { + let uri = request.uri().to_string(); + + let request_f = self.request(request); + let response = drive03(request_f) + .await? + .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; + let status = response.status(); + let headers = response.headers().clone(); + let body_bytes = hyper::body::to_bytes(response.into_body()) + .await + .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; + let body_str = String::from_utf8(body_bytes.to_vec()).map_to_mm(|e| SlurpError::Internal(e.to_string()))?; + let body: Json = serde_json::from_str(&body_str)?; + Ok((status, headers, body)) + } + + /// Executes a Hyper request, returning the response status, headers and body. + async fn slurp_req(&self, request: Request>) -> SlurpResult { + let uri = request.uri().to_string(); + let (head, body) = request.into_parts(); + let request = Request::from_parts(head, Body::from(body)); + let request_f = self.request(request); + let response = drive03(request_f) + .await? + .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; + let status = response.status(); + let headers = response.headers().clone(); + let body = response.into_body(); + let output = hyper::body::to_bytes(body) + .await + .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; + Ok((status, headers, output.to_vec())) + } +} + +#[async_trait] +impl SlurpHttpClient for Client +where + C: Connect + Clone + Send + Sync + 'static, +{ + fn request(&self, req: Request) -> ResponseFuture { Client::::request(self, req) } +} + +/// Executes a Hyper request, returning the response status, headers and body. +pub async fn slurp_req(request: Request>) -> SlurpResult { HYPER.slurp_req(request).await } + +/// Executes a Hyper request, requires [`Request`] and return the response status, headers and body as Json. +pub async fn slurp_req_body(request: Request) -> SlurpResultJson { HYPER.slurp_req_body(request).await } + +/// Executes a GET request, returning the response status, headers and body. +pub async fn slurp_url(url: &str) -> SlurpResult { HYPER.slurp_url(url).await } + +/// Executes a GET request with additional headers. +/// Returning the response status, headers and body. +pub async fn slurp_url_with_headers(url: &str, headers: Vec<(&'static str, &'static str)>) -> SlurpResult { + HYPER.slurp_url_with_headers(url, headers).await +} + +/// Executes a POST request, returning the response status, headers and body. +pub async fn slurp_post_json(url: &str, body: String) -> SlurpResult { HYPER.slurp_post_json(url, body).await } + impl From for SlurpError { fn from(_: Canceled) -> Self { SlurpError::Internal("Spawned Slurp future has been canceled".to_owned()) } } @@ -31,76 +231,6 @@ impl From for SlurpError { fn from(e: http::Error) -> Self { SlurpError::InvalidRequest(e.to_string()) } } -/// Executes a Hyper request, returning the response status, headers and body. -pub async fn slurp_req(request: Request>) -> SlurpResult { - let uri = request.uri().to_string(); - let (head, body) = request.into_parts(); - let request = Request::from_parts(head, Body::from(body)); - - let request_f = HYPER.request(request); - let response = drive03(request_f) - .await? - .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; - let status = response.status(); - let headers = response.headers().clone(); - let body = response.into_body(); - let output = hyper::body::to_bytes(body) - .await - .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; - Ok((status, headers, output.to_vec())) -} - -/// Executes a Hyper request, requires [`Request`] and return the response status, headers and body as Json. -pub async fn slurp_req_body(request: Request) -> SlurpResultJson { - let uri = request.uri().to_string(); - - let request_f = HYPER.request(request); - let response = drive03(request_f) - .await? - .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; - let status = response.status(); - let headers = response.headers().clone(); - // Get the response body bytes. - let body_bytes = hyper::body::to_bytes(response.into_body()) - .await - .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; - let body_str = String::from_utf8(body_bytes.to_vec()).map_to_mm(|e| SlurpError::Internal(e.to_string()))?; - let body: Json = serde_json::from_str(&body_str)?; - Ok((status, headers, body)) -} - -/// Executes a GET request, returning the response status, headers and body. -pub async fn slurp_url(url: &str) -> SlurpResult { - let req = Request::builder().uri(url).body(Vec::new())?; - slurp_req(req).await -} - -/// Executes a GET request with additional headers. -/// Returning the response status, headers and body. -pub async fn slurp_url_with_headers(url: &str, headers: Vec<(&'static str, &'static str)>) -> SlurpResult { - let mut req = Request::builder(); - let h = req - .headers_mut() - .or_mm_err(|| SlurpError::Internal("An error occured while accessing to the request headers.".to_string()))?; - - for (key, value) in headers { - h.insert(key, HeaderValue::from_static(value)); - } - - let req = req.uri(url).body(Vec::new())?; - slurp_req(req).await -} - -/// Executes a POST request, returning the response status, headers and body. -pub async fn slurp_post_json(url: &str, body: String) -> SlurpResult { - let request = Request::builder() - .method("POST") - .uri(url) - .header(header::CONTENT_TYPE, APPLICATION_JSON) - .body(body.into())?; - slurp_req(request).await -} - #[cfg(test)] mod tests { use crate::native_http::slurp_url;