From 7d09cdefce5dcb266996c81df23381c7014e0cdf Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Tue, 24 Sep 2024 16:49:18 -0700 Subject: [PATCH 1/2] Allow byob http behind feature flag --- Cargo.toml | 4 +- crates/web5/Cargo.toml | 6 + .../web5/src/credentials/credential_schema.rs | 23 +- crates/web5/src/dids/methods/did_dht/mod.rs | 22 +- .../web5/src/dids/methods/did_web/resolver.rs | 21 +- crates/web5/src/http.rs | 339 ++++++++---------- crates/web5/src/lib.rs | 2 + 7 files changed, 208 insertions(+), 209 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f7b9cef4..ae6475fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] -members = [ - "crates/web5", +members = [ + "crates/web5", "crates/web5_cli", "bindings/web5_uniffi", "bindings/web5_uniffi_wrapper", diff --git a/crates/web5/Cargo.toml b/crates/web5/Cargo.toml index b4468953..cba3cd21 100644 --- a/crates/web5/Cargo.toml +++ b/crates/web5/Cargo.toml @@ -31,6 +31,12 @@ lazy_static = "1.5.0" flate2 = "1.0.33" rustls = { version = "0.23.13", default-features = false, features = ["std", "tls12"] } rustls-native-certs = "0.8.0" +reqwest = { version = "0.12", optional = true, features = ["blocking"]} +once_cell = "1.19.0" + +[features] +default = ["default_http_client"] +default_http_client = ["reqwest"] [dev-dependencies] mockito = "1.5.0" diff --git a/crates/web5/src/credentials/credential_schema.rs b/crates/web5/src/credentials/credential_schema.rs index 56ab9898..2b1ebc7d 100644 --- a/crates/web5/src/credentials/credential_schema.rs +++ b/crates/web5/src/credentials/credential_schema.rs @@ -1,7 +1,9 @@ +use std::collections::HashMap; + use super::verifiable_credential_1_1::VerifiableCredential; use crate::{ errors::{Result, Web5Error}, - http::get_json, + http::get_http_client, }; use jsonschema::{Draft, JSONSchema}; use serde::{Deserialize, Serialize}; @@ -30,7 +32,24 @@ pub(crate) fn validate_credential_schema( } let url = &credential_schema.id; - let json_schema = get_json::(url)?; + + let headers: HashMap = HashMap::from([ + ("Host".to_string(), "{}".to_string()), + ("Connection".to_string(), "close".to_string()), + ("Accept".to_string(), "application/json".to_string()) + ]); + let response = get_http_client().get(url, Some(headers)) + .map_err(|e| Web5Error::Network(format!("Failed to fetch credential schema: {}", e)))?; + if !(200..300).contains(&response.status_code) { + return Err(Web5Error::Http(format!( + "Failed to fetch credential schema: non-successful response code {}", + response.status_code + ))); + } + + let json_schema = serde_json::from_slice::(&response.body) + .map_err(|err| Web5Error::Http(format!("unable to parse json response body {}", err)))?; + let compiled_schema = JSONSchema::options().compile(&json_schema).map_err(|err| { Web5Error::JsonSchema(format!("unable to compile json schema {} {}", url, err)) })?; diff --git a/crates/web5/src/dids/methods/did_dht/mod.rs b/crates/web5/src/dids/methods/did_dht/mod.rs index ca910544..84e14d7f 100644 --- a/crates/web5/src/dids/methods/did_dht/mod.rs +++ b/crates/web5/src/dids/methods/did_dht/mod.rs @@ -18,9 +18,9 @@ use crate::{ }, }, errors::{Result, Web5Error}, - http::{get, put}, + http::get_http_client, }; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; mod bep44; mod document_packet; @@ -191,7 +191,16 @@ impl DidDht { bearer_did.did.id.trim_start_matches('/') ); - let response = put(&url, &body)?; + let headers: HashMap = HashMap::from([ + ("Host".to_string(), "{}".to_string()), + ("Connection".to_string(), "close".to_string()), + ("Content-Length".to_string(), "{}".to_string()), + ("Content-Type".to_string(), "application/octet-stream".to_string()) + ]); + + let response = get_http_client().put(&url, Some(headers), &body) + .map_err(|e| Web5Error::Network(format!("Failed to PUT did:dht: {}", e)))?; + if response.status_code != 200 { return Err(Web5Error::Network( "failed to PUT DID to mainline".to_string(), @@ -248,7 +257,12 @@ impl DidDht { did.id.trim_start_matches('/') ); - let response = get(&url).map_err(|_| ResolutionMetadataError::InternalError)?; + let headers: HashMap = HashMap::from([ + ("Host".to_string(), "{}".to_string()), + ("Connection".to_string(), "close".to_string()), + ("Accept".to_string(), "application/octet-stream".to_string()) + ]); + let response = get_http_client().get(&url, Some(headers)).map_err(|_| ResolutionMetadataError::InternalError)?; if response.status_code == 404 { return Err(ResolutionMetadataError::NotFound); diff --git a/crates/web5/src/dids/methods/did_web/resolver.rs b/crates/web5/src/dids/methods/did_web/resolver.rs index 2ab57612..f683da61 100644 --- a/crates/web5/src/dids/methods/did_web/resolver.rs +++ b/crates/web5/src/dids/methods/did_web/resolver.rs @@ -5,12 +5,10 @@ use crate::{ resolution::{ resolution_metadata::ResolutionMetadataError, resolution_result::ResolutionResult, }, - }, - http::get_json, + }, http::get_http_client, }; use std::{ - future::{Future, IntoFuture}, - pin::Pin, + collections::HashMap, future::{Future, IntoFuture}, pin::Pin }; use url::Url; @@ -50,8 +48,19 @@ impl Resolver { } async fn resolve(url: String) -> Result { - let document = - get_json::(&url).map_err(|_| ResolutionMetadataError::InternalError)?; + let headers: HashMap = HashMap::from([ + ("Host".to_string(), "{}".to_string()), + ("Connection".to_string(), "close".to_string()), + ("Accept".to_string(), "application/json".to_string()) + ]); + let response = get_http_client().get(&url, Some(headers)) + .map_err(|_| ResolutionMetadataError::InternalError)?; + if !(200..300).contains(&response.status_code) { + return Err(ResolutionMetadataError::InternalError); + } + + let document = serde_json::from_slice::(&response.body) + .map_err(|_| ResolutionMetadataError::InternalError)?; Ok(ResolutionResult { document: Some(document), ..Default::default() diff --git a/crates/web5/src/http.rs b/crates/web5/src/http.rs index ab7c627b..7d6f3403 100644 --- a/crates/web5/src/http.rs +++ b/crates/web5/src/http.rs @@ -1,13 +1,28 @@ -use crate::errors::{Result, Web5Error}; -use rustls::pki_types::ServerName; -use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned}; -use rustls_native_certs::load_native_certs; -use serde::de::DeserializeOwned; -use std::collections::HashMap; -use std::io::{Read, Write}; -use std::net::TcpStream; -use std::sync::Arc; -use url::Url; + +use std::{collections::HashMap, sync::Arc}; +use once_cell::sync::OnceCell; + +pub trait HttpClient: Send + Sync { + fn get( + &self, + url: &str, + headers: Option> + ) -> std::result::Result>; + + fn post( + &self, + url: &str, + headers: Option>, + body: &[u8] + ) -> std::result::Result>; + + fn put( + &self, + url: &str, + headers: Option>, + body: &[u8] + ) -> std::result::Result>; +} pub struct HttpResponse { pub status_code: u16, @@ -16,205 +31,139 @@ pub struct HttpResponse { pub body: Vec, } -struct Destination { - pub host: String, - pub path: String, - pub port: u16, - pub schema: String, -} - -fn parse_destination(url: &str) -> Result { - let parsed_url = - Url::parse(url).map_err(|err| Web5Error::Http(format!("failed to parse url {}", err)))?; - - let host = parsed_url - .host_str() - .ok_or_else(|| Web5Error::Http(format!("url must have a host: {}", url)))?; +static HTTP_CLIENT: OnceCell> = OnceCell::new(); - let path = if parsed_url.path().is_empty() { - "/".to_string() - } else { - parsed_url.path().to_string() - }; - - let port = parsed_url - .port_or_known_default() - .ok_or_else(|| Web5Error::Http("unable to determine port".to_string()))?; - - let schema = parsed_url.scheme().to_string(); - - Ok(Destination { - host: host.to_string(), - path, - port, - schema, - }) +#[cfg(feature = "default_http_client")] +pub fn get_http_client() -> &'static dyn HttpClient { + HTTP_CLIENT.get_or_init(|| { + Arc::new(reqwest_http_client::ReqwestHttpClient::new()) + }).as_ref() } -fn transmit(destination: &Destination, request: &[u8]) -> Result> { - let mut buffer = Vec::new(); - - if destination.schema == "https" { - // HTTPS connection - - // Create a RootCertStore and load the root certificates from rustls_native_certs - let mut root_store = RootCertStore::empty(); - for cert in load_native_certs().unwrap() { - root_store.add(cert).unwrap(); - } - - // Build the ClientConfig using the root certificates and disabling client auth - let config = ClientConfig::builder() - .with_root_certificates(root_store) - .with_no_client_auth(); - - let rc_config = Arc::new(config); // Arc allows sharing the config - - // Make the TCP connection to the server - let stream = TcpStream::connect((&destination.host[..], destination.port)) - .map_err(|err| Web5Error::Network(format!("failed to connect to host: {}", err)))?; - - // Convert the server name to the expected type for TLS validation - let server_name = ServerName::try_from(destination.host.clone()) - .map_err(|_| Web5Error::Http("invalid DNS name".to_string()))?; - - // Create the TLS connection - let client = ClientConnection::new(rc_config, server_name) - .map_err(|err| Web5Error::Network(err.to_string()))?; - let mut tls_stream = StreamOwned::new(client, stream); - - // Write the request over the TLS stream - tls_stream - .write_all(request) - .map_err(|err| Web5Error::Network(err.to_string()))?; - - // Read the response into the buffer - tls_stream - .read_to_end(&mut buffer) - .map_err(|err| Web5Error::Network(err.to_string()))?; - } else { - // HTTP connection - let mut stream = TcpStream::connect((&destination.host[..], destination.port)) - .map_err(|err| Web5Error::Network(format!("failed to connect to host: {}", err)))?; - - stream - .write_all(request) - .map_err(|err| Web5Error::Network(err.to_string()))?; - - stream - .read_to_end(&mut buffer) - .map_err(|err| Web5Error::Network(err.to_string()))?; - } - - Ok(buffer) +#[cfg(not(feature = "default_http_client"))] +pub fn get_http_client() -> &'static dyn HttpClient { + HTTP_CLIENT.get().expect("HttpClient has not been set. Please call set_http_client().").as_ref() } -fn parse_response(response_bytes: &[u8]) -> Result { - // Find the position of the first \r\n\r\n, which separates headers and body - let header_end = response_bytes - .windows(4) - .position(|window| window == b"\r\n\r\n") - .ok_or_else(|| Web5Error::Http("invalid HTTP response format".to_string()))?; - - // Extract the headers section (before the \r\n\r\n) - let header_part = &response_bytes[..header_end]; +#[cfg(feature = "default_http_client")] +pub fn set_http_client() { + panic!("Cannot set a custom HttpClient when the reqwest feature is enabled."); +} - // Convert the header part to a string (since headers are ASCII/UTF-8 compliant) - let header_str = String::from_utf8_lossy(header_part); +#[cfg(not(feature = "default_http_client"))] +pub fn set_http_client() { + HTTP_CLIENT.set(client).unwrap_or_else(|_| panic!("HttpClient has already been set.")); +} - // Parse the status line (first line in the headers) - let mut header_lines = header_str.lines(); - let status_line = header_lines - .next() - .ok_or_else(|| Web5Error::Http("missing status line".to_string()))?; +#[cfg(feature = "default_http_client")] +mod reqwest_http_client { + use super::*; + use reqwest::blocking::Client; + use std::collections::HashMap; - let status_parts: Vec<&str> = status_line.split_whitespace().collect(); - if status_parts.len() < 3 { - return Err(Web5Error::Http("invalid status line format".to_string())); + pub struct ReqwestHttpClient { + client: Client, } - let status_code = status_parts[1] - .parse::() - .map_err(|_| Web5Error::Http("invalid status code".to_string()))?; - - // Parse headers into a HashMap - let mut headers = HashMap::new(); - for line in header_lines { - if let Some((key, value)) = line.split_once(": ") { - headers.insert(key.to_string(), value.to_string()); + impl ReqwestHttpClient { + pub fn new() -> Self { + ReqwestHttpClient { + client: Client::new(), + } } } - // The body is the part after the \r\n\r\n separator - let body = response_bytes[header_end + 4..].to_vec(); + impl HttpClient for ReqwestHttpClient { + fn get( + &self, + url: &str, + headers: Option>, + ) -> Result> { + let mut req = self.client.get(url); + + if let Some(headers) = headers { + for (key, value) in headers { + req = req.header(&key, &value); + } + } + + let response = req.send()?.error_for_status()?; + let status_code = response.status().as_u16(); + let headers = response + .headers() + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) + .collect(); + + let body = response.bytes()?.to_vec(); + + Ok(HttpResponse { + status_code, + headers, + body, + }) + } - Ok(HttpResponse { - status_code, - headers, - body, - }) -} + fn post( + &self, + url: &str, + headers: Option>, + body: &[u8], + ) -> Result> { + let mut req = self.client.post(url).body(body.to_vec()); + + if let Some(headers) = headers { + for (key, value) in headers { + req = req.header(&key, &value); + } + } + + let response = req.send()?.error_for_status()?; + let status_code = response.status().as_u16(); + let headers = response + .headers() + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) + .collect(); + + let body = response.bytes()?.to_vec(); + + Ok(HttpResponse { + status_code, + headers, + body, + }) + } -pub fn get_json(url: &str) -> Result { - let destination = parse_destination(url)?; - let request = format!( - "GET {} HTTP/1.1\r\n\ - Host: {}\r\n\ - Connection: close\r\n\ - Accept: application/json\r\n\r\n", - destination.path, destination.host - ); - let response_bytes = transmit(&destination, request.as_bytes())?; - let response = parse_response(&response_bytes)?; - - if !(200..300).contains(&response.status_code) { - return Err(Web5Error::Http(format!( - "non-successful response code {}", - response.status_code - ))); + fn put( + &self, + url: &str, + headers: Option>, + body: &[u8], + ) -> Result> { + let mut req = self.client.put(url).body(body.to_vec()); + + if let Some(headers) = headers { + for (key, value) in headers { + req = req.header(&key, &value); + } + } + + let response = req.send()?.error_for_status()?; + let status_code = response.status().as_u16(); + let headers = response + .headers() + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) + .collect(); + + let body = response.bytes()?.to_vec(); + + Ok(HttpResponse { + status_code, + headers, + body, + }) + } } - - let json_value = serde_json::from_slice::(&response.body) - .map_err(|err| Web5Error::Http(format!("unable to parse json response body {}", err)))?; - - Ok(json_value) -} - -pub fn get(url: &str) -> Result { - let destination = parse_destination(url)?; - - let request = format!( - "GET {} HTTP/1.1\r\n\ - Host: {}\r\n\ - Connection: close\r\n\ - Accept: application/octet-stream\r\n\r\n", - destination.path, destination.host - ); - - let response_bytes = transmit(&destination, request.as_bytes())?; - - parse_response(&response_bytes) -} - -pub fn put(url: &str, body: &[u8]) -> Result { - let destination = parse_destination(url)?; - - let request = format!( - "PUT {} HTTP/1.1\r\n\ - Host: {}\r\n\ - Connection: close\r\n\ - Content-Length: {}\r\n\ - Content-Type: application/octet-stream\r\n\r\n", - destination.path, - destination.host, - body.len() - ); - - // Concatenate the request headers and the body to form the full request - let mut request_with_body = request.into_bytes(); - request_with_body.extend_from_slice(body); - - let response_bytes = transmit(&destination, &request_with_body)?; - - parse_response(&response_bytes) } diff --git a/crates/web5/src/lib.rs b/crates/web5/src/lib.rs index 1806cd43..73eac559 100644 --- a/crates/web5/src/lib.rs +++ b/crates/web5/src/lib.rs @@ -8,5 +8,7 @@ mod http; mod jose; pub mod json; +pub use http::set_http_client; + #[cfg(test)] mod test_vectors; From 17a889f9776ef38e1723eddd973bcf2b5750a3c7 Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Wed, 25 Sep 2024 10:24:34 -0700 Subject: [PATCH 2/2] Fix non async test breakages --- crates/web5/src/credentials/create.rs | 6 +++--- crates/web5/src/credentials/verifiable_credential_1_1.rs | 4 ++-- crates/web5/src/http.rs | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/web5/src/credentials/create.rs b/crates/web5/src/credentials/create.rs index 9fae7ab5..07043c03 100644 --- a/crates/web5/src/credentials/create.rs +++ b/crates/web5/src/credentials/create.rs @@ -572,7 +572,7 @@ mod tests { match result { Err(Web5Error::Network(err_msg)) => { assert!( - err_msg.contains("failed to connect to host"), + err_msg.contains("Failed to fetch credential schema"), "Error message is: {}", err_msg ) @@ -605,10 +605,10 @@ mod tests { match result { Err(Web5Error::Http(err_msg)) => { - assert_eq!("non-successful response code 500", err_msg) + assert!(err_msg.contains("non-successful response code 500")) } _ => panic!( - "expected Web5Error::JsonSchema with specific message but got {:?}", + "expected Web5Error::Http with specific message but got {:?}", result ), } diff --git a/crates/web5/src/credentials/verifiable_credential_1_1.rs b/crates/web5/src/credentials/verifiable_credential_1_1.rs index 50ac1c9d..42b5ba86 100644 --- a/crates/web5/src/credentials/verifiable_credential_1_1.rs +++ b/crates/web5/src/credentials/verifiable_credential_1_1.rs @@ -798,7 +798,7 @@ mod tests { match result { Err(Web5Error::Network(err_msg)) => { assert!( - err_msg.contains("failed to connect to host"), + err_msg.contains("Failed to fetch credential schema"), "Error message is: {}", err_msg ) @@ -827,7 +827,7 @@ mod tests { let result = VerifiableCredential::from_vc_jwt(vc_jwt_at_port, true); match result { Err(Web5Error::Http(err_msg)) => { - assert_eq!("non-successful response code 500", err_msg) + assert!(err_msg.contains("non-successful response code 500")) } _ => panic!( "expected Web5Error::JsonSchema with specific message but got {:?}", diff --git a/crates/web5/src/http.rs b/crates/web5/src/http.rs index 7d6f3403..4635dfaf 100644 --- a/crates/web5/src/http.rs +++ b/crates/web5/src/http.rs @@ -87,7 +87,7 @@ mod reqwest_http_client { } } - let response = req.send()?.error_for_status()?; + let response = req.send()?; let status_code = response.status().as_u16(); let headers = response .headers() @@ -118,7 +118,7 @@ mod reqwest_http_client { } } - let response = req.send()?.error_for_status()?; + let response = req.send()?; let status_code = response.status().as_u16(); let headers = response .headers() @@ -149,7 +149,7 @@ mod reqwest_http_client { } } - let response = req.send()?.error_for_status()?; + let response = req.send()?; let status_code = response.status().as_u16(); let headers = response .headers()