diff --git a/Cargo.toml b/Cargo.toml index 2bbeb646..b25317c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ members = [ "bindings/web5_uniffi", "bindings/web5_uniffi_wrapper", "bindings/web5_wasm", - "crates/http-std", "crates/web5", "crates/web5_cli", ] diff --git a/bindings/web5_uniffi/libtargets/x86_64_unknown_linux_gnu/Dockerfile b/bindings/web5_uniffi/libtargets/x86_64_unknown_linux_gnu/Dockerfile index 5446db11..a97b68d6 100644 --- a/bindings/web5_uniffi/libtargets/x86_64_unknown_linux_gnu/Dockerfile +++ b/bindings/web5_uniffi/libtargets/x86_64_unknown_linux_gnu/Dockerfile @@ -19,7 +19,6 @@ COPY bindings/web5_c ./bindings/web5_c COPY bindings/web5_uniffi_wrapper ./bindings/web5_uniffi_wrapper COPY bindings/web5_uniffi ./bindings/web5_uniffi COPY bindings/web5_wasm ./bindings/web5_wasm -COPY crates/http-std ./crates/http-std COPY crates/web5 ./crates/web5 COPY crates/web5_cli ./crates/web5_cli diff --git a/bindings/web5_uniffi/libtargets/x86_64_unknown_linux_musl/Dockerfile b/bindings/web5_uniffi/libtargets/x86_64_unknown_linux_musl/Dockerfile index b1f17203..cdc6f609 100644 --- a/bindings/web5_uniffi/libtargets/x86_64_unknown_linux_musl/Dockerfile +++ b/bindings/web5_uniffi/libtargets/x86_64_unknown_linux_musl/Dockerfile @@ -36,7 +36,6 @@ COPY bindings/web5_c ./bindings/web5_c COPY bindings/web5_uniffi_wrapper ./bindings/web5_uniffi_wrapper COPY bindings/web5_uniffi ./bindings/web5_uniffi COPY bindings/web5_wasm ./bindings/web5_wasm -COPY crates/http-std ./crates/http-std COPY crates/web5 ./crates/web5 COPY crates/web5_cli ./crates/web5_cli diff --git a/bindings/web5_uniffi_wrapper/Cargo.toml b/bindings/web5_uniffi_wrapper/Cargo.toml index d8621950..12debcd9 100644 --- a/bindings/web5_uniffi_wrapper/Cargo.toml +++ b/bindings/web5_uniffi_wrapper/Cargo.toml @@ -10,4 +10,4 @@ license-file.workspace = true serde_json = { workspace = true } thiserror = { workspace = true } tokio = { version = "1.38.0", features = ["full"] } -web5 = { path = "../../crates/web5" } \ No newline at end of file +web5 = { path = "../../crates/web5", features = ["http_reqwest"]} \ No newline at end of file diff --git a/bindings/web5_uniffi_wrapper/src/dids/methods/did_web.rs b/bindings/web5_uniffi_wrapper/src/dids/methods/did_web.rs index 49d218cd..fa567299 100644 --- a/bindings/web5_uniffi_wrapper/src/dids/methods/did_web.rs +++ b/bindings/web5_uniffi_wrapper/src/dids/methods/did_web.rs @@ -1,7 +1,8 @@ use crate::{ crypto::key_manager::{KeyManager, ToInnerKeyManager}, dids::{bearer_did::BearerDid, resolution::resolution_result::ResolutionResult}, - errors::Result, get_rt, + errors::Result, + get_rt, }; use std::sync::Arc; use web5::{ diff --git a/bound/kt/src/test/kotlin/web5/sdk/vc/VerifiableCredentialTest.kt b/bound/kt/src/test/kotlin/web5/sdk/vc/VerifiableCredentialTest.kt index caf0f4dd..e8979a18 100644 --- a/bound/kt/src/test/kotlin/web5/sdk/vc/VerifiableCredentialTest.kt +++ b/bound/kt/src/test/kotlin/web5/sdk/vc/VerifiableCredentialTest.kt @@ -322,7 +322,7 @@ class VerifiableCredentialTest { VerifiableCredential.create(ISSUER, CREDENTIAL_SUBJECT, options) } - assertTrue(exception.message.contains("failed to resolve status code 500")) + assertTrue(exception.message.contains("Failed to fetch credential schema")) mockWebServer.shutdown() } @@ -1054,7 +1054,7 @@ class VerifiableCredentialTest { VerifiableCredential.fromVcJwt(vcJwtAtPort, true) } - assertTrue(exception.message.contains("failed to resolve status code 500")) + assertTrue(exception.message.contains("Failed to fetch credential schema")) mockWebServer.shutdown() } diff --git a/bound/typescript/package-lock.json b/bound/typescript/package-lock.json index 55a48130..a4a7cb36 100644 --- a/bound/typescript/package-lock.json +++ b/bound/typescript/package-lock.json @@ -7,15 +7,12 @@ "": { "name": "web5", "version": "0.1.0", - "dependencies": { - "base64url": "^3.0.1" - }, "devDependencies": { - "@types/base64url": "^2.0.0", "@types/chai": "4.3.0", "@types/mocha": "9.1.0", "@web/test-runner": "0.18.0", "@web/test-runner-playwright": "0.11.0", + "base64url": "^3.0.1", "chai": "4.3.10", "esbuild": "0.19.9", "mocha": "10.2.0", @@ -1006,16 +1003,6 @@ "integrity": "sha512-Anitqkl3+KrzcW2k77lRlg/GfLZLWXBuNgbEcIOU6M92yw42vsd3xV/Z/yAHEj8m+KUjL6bWOVOFqX8PFPJ4LA==", "dev": true }, - "node_modules/@types/base64url": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/base64url/-/base64url-2.0.0.tgz", - "integrity": "sha512-r+d5HWla1etMB9MdDr/wfzhiWdT6GTlO6Xp96FnSsnsxeOi3ZCz1EJyv7OE7Coc2cw5tGblqahMYNL/KwdP8kQ==", - "deprecated": "This is a stub types definition for base64url (https://github.com/brianloveswords/base64url). base64url provides its own type definitions, so you don't need @types/base64url installed!", - "dev": true, - "dependencies": { - "base64url": "*" - } - }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -1715,6 +1702,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "dev": true, "engines": { "node": ">=6.0.0" } diff --git a/crates/http-std/Cargo.toml b/crates/http-std/Cargo.toml deleted file mode 100644 index 4b85f61d..00000000 --- a/crates/http-std/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "http-std" -version = "0.1.0" -edition = "2021" -homepage.workspace = true -repository.workspace = true -license-file.workspace = true - -[dependencies] -async-trait = "0.1.83" -serde = { workspace = true } -lazy_static = { workspace = true } -thiserror = { workspace = true } -url = "2.5.0" - -[target.'cfg(not(target_family = "wasm"))'.dependencies] -reqwest = { version = "0.12.7", features = ["blocking"] } -rustls = { version = "0.23.13", default-features = false, features = [ - "std", - "tls12", -] } -rustls-native-certs = "0.8.0" diff --git a/crates/http-std/src/client.rs b/crates/http-std/src/client.rs deleted file mode 100644 index e5673d7a..00000000 --- a/crates/http-std/src/client.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::{Error, Result}; -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fmt, str::FromStr}; - -#[async_trait] -pub trait Client: Send + Sync { - async fn fetch(&self, url: &str, options: Option) -> Result; -} - -#[derive(Default, Serialize, Deserialize)] -pub struct FetchOptions { - pub method: Option, - pub headers: Option>, - pub body: Option>, -} - -#[derive(Serialize, Deserialize)] -pub struct Response { - pub status_code: u16, - pub headers: HashMap, - pub body: Vec, -} - -#[derive(Serialize, Deserialize)] -pub enum Method { - Get, - Post, - Put, -} - -impl fmt::Display for Method { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let method_str = match self { - Method::Get => "GET", - Method::Post => "POST", - Method::Put => "PUT", - }; - write!(f, "{}", method_str) - } -} - -impl FromStr for Method { - type Err = Error; - - fn from_str(s: &str) -> Result { - match s.to_ascii_uppercase().as_ref() { - "GET" => Ok(Method::Get), - "POST" => Ok(Method::Post), - "PUT" => Ok(Method::Put), - _ => Err(Error::Parameter(format!("unknown method {}", s))), - } - } -} diff --git a/crates/http-std/src/default_client.rs b/crates/http-std/src/default_client.rs deleted file mode 100644 index e7c2d56f..00000000 --- a/crates/http-std/src/default_client.rs +++ /dev/null @@ -1,193 +0,0 @@ -use crate::{Client, Error, FetchOptions, Method, Response, Result}; -use async_trait::async_trait; -use rustls::pki_types::ServerName; -use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned}; -use rustls_native_certs::load_native_certs; -use std::collections::HashMap; -use std::io::{Read, Write}; -use std::net::TcpStream; -use std::sync::Arc; -use std::time::Duration; -use url::Url; - -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| Error::Parameter(format!("failed to parse url {}", err)))?; - - let host = parsed_url - .host_str() - .ok_or_else(|| Error::Parameter(format!("url must have a host: {}", url)))?; - - 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(|| Error::Parameter("unable to determine port".to_string()))?; - - let schema = parsed_url.scheme().to_string(); - - Ok(Destination { - host: host.to_string(), - path, - port, - schema, - }) -} - -fn transmit(destination: &Destination, request: &[u8]) -> Result> { - let mut buffer = Vec::new(); - - if destination.schema == "https" { - let mut root_store = RootCertStore::empty(); - for cert in load_native_certs().unwrap() { - root_store.add(cert).unwrap(); - } - - let config = ClientConfig::builder() - .with_root_certificates(root_store) - .with_no_client_auth(); - - let rc_config = Arc::new(config); - - let stream = TcpStream::connect((&destination.host[..], destination.port)) - .map_err(|err| Error::Network(format!("failed to connect to host: {}", err)))?; - - let server_name = ServerName::try_from(destination.host.clone()) - .map_err(|_| Error::Network("invalid DNS name".to_string()))?; - - let client = ClientConnection::new(rc_config, server_name) - .map_err(|err| Error::Network(err.to_string()))?; - let mut tls_stream = StreamOwned::new(client, stream); - - tls_stream - .get_ref() - .set_read_timeout(Some(Duration::from_secs(60))) - .map_err(|err| Error::Network(err.to_string()))?; - tls_stream - .get_ref() - .set_write_timeout(Some(Duration::from_secs(60))) - .map_err(|err| Error::Network(err.to_string()))?; - - tls_stream - .write_all(request) - .map_err(|err| Error::Network(err.to_string()))?; - - tls_stream - .flush() - .map_err(|err| Error::Network(err.to_string()))?; - - tls_stream - .read_to_end(&mut buffer) - .map_err(|err| Error::Network(err.to_string()))?; - } else { - let mut stream = TcpStream::connect((&destination.host[..], destination.port)) - .map_err(|err| Error::Network(format!("failed to connect to host: {}", err)))?; - - stream - .set_read_timeout(Some(Duration::from_secs(60))) - .map_err(|err| Error::Network(err.to_string()))?; - stream - .set_write_timeout(Some(Duration::from_secs(60))) - .map_err(|err| Error::Network(err.to_string()))?; - - stream - .write_all(request) - .map_err(|err| Error::Network(err.to_string()))?; - - stream - .flush() - .map_err(|err| Error::Network(err.to_string()))?; - - stream - .read_to_end(&mut buffer) - .map_err(|err| Error::Network(err.to_string()))?; - } - - Ok(buffer) -} - -fn parse_response(response_bytes: &[u8]) -> Result { - let header_end = response_bytes - .windows(4) - .position(|window| window == b"\r\n\r\n") - .ok_or_else(|| Error::Response("invalid HTTP response format".to_string()))?; - - let header_part = &response_bytes[..header_end]; - - let header_str = String::from_utf8_lossy(header_part); - - let mut header_lines = header_str.lines(); - let status_line = header_lines - .next() - .ok_or_else(|| Error::Response("missing status line".to_string()))?; - - let status_parts: Vec<&str> = status_line.split_whitespace().collect(); - if status_parts.len() < 3 { - return Err(Error::Response("invalid status line format".to_string())); - } - - let status_code = status_parts[1] - .parse::() - .map_err(|_| Error::Response("invalid status code".to_string()))?; - - 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()); - } - } - - let body = response_bytes[header_end + 4..].to_vec(); - - Ok(Response { - status_code, - headers, - body, - }) -} - -pub struct DefaultClient; - -#[async_trait] -impl Client for DefaultClient { - async fn fetch(&self, url: &str, options: Option) -> Result { - let options = options.unwrap_or_default(); - let destination = parse_destination(url)?; - let method = options.method.unwrap_or(Method::Get); - - let mut request = format!( - "{} {} HTTP/1.1\r\n\ - Host: {}\r\n\ - Connection: close\r\n", - method, destination.path, destination.host, - ); - if let Some(headers) = &options.headers { - if !headers.is_empty() { - for (key, value) in headers { - request.push_str(&format!("{}: {}\r\n", key, value)); - } - } - } - request.push_str("\r\n"); - - let mut request_bytes = request.into_bytes(); - if let Some(body) = &options.body { - request_bytes.extend_from_slice(body); - } - - let response_bytes = transmit(&destination, &request_bytes)?; - - parse_response(&response_bytes) - } -} diff --git a/crates/http-std/src/error.rs b/crates/http-std/src/error.rs deleted file mode 100644 index 8075c271..00000000 --- a/crates/http-std/src/error.rs +++ /dev/null @@ -1,26 +0,0 @@ -#[cfg(not(target_arch = "wasm32"))] -use reqwest::Error as ReqwestError; - -#[derive(thiserror::Error, Debug, Clone, PartialEq)] -pub enum Error { - #[error("unknown error {0}")] - Unknown(String), - #[error("parameter error {0}")] - Parameter(String), - #[error("network error {0}")] - Network(String), - #[error("response error {0}")] - Response(String), - - #[error("reqwest error {0}")] - Reqwest(String), -} - -#[cfg(not(target_arch = "wasm32"))] -impl From for Error { - fn from(err: ReqwestError) -> Self { - Error::Reqwest(err.to_string()) - } -} - -pub type Result = std::result::Result; diff --git a/crates/http-std/src/lib.rs b/crates/http-std/src/lib.rs deleted file mode 100644 index 7c23bab3..00000000 --- a/crates/http-std/src/lib.rs +++ /dev/null @@ -1,51 +0,0 @@ -mod client; -#[cfg(not(target_arch = "wasm32"))] -mod default_client; -mod error; -#[cfg(not(target_arch = "wasm32"))] -mod reqwest_client; - -#[cfg(target_arch = "wasm32")] -use async_trait::async_trait; -use lazy_static::lazy_static; -use std::sync::{Arc, Mutex}; - -pub use client::{Client, FetchOptions, Method, Response}; -pub use error::{Error, Result}; - -#[cfg(not(target_arch = "wasm32"))] -lazy_static! { - static ref CLIENT: Mutex> = - Mutex::new(Arc::new(reqwest_client::ReqwestClient::new())); -} - -#[cfg(target_arch = "wasm32")] -lazy_static! { - static ref CLIENT: Mutex> = Mutex::new(Arc::new(ForeignEmptyClient)); -} - -pub fn set_client(client: Arc) { - let mut global_client = CLIENT.lock().unwrap(); - *global_client = client; -} - -pub fn get_client() -> Arc { - let client = CLIENT.lock().unwrap(); - client.clone() -} - -pub async fn fetch(url: &str, options: Option) -> Result { - let client = get_client(); - client.fetch(url, options).await -} - -#[cfg(target_arch = "wasm32")] -pub struct ForeignEmptyClient; - -#[cfg(target_arch = "wasm32")] -#[async_trait] -impl Client for ForeignEmptyClient { - async fn fetch(&self, _url: &str, _options: Option) -> Result { - return Err(Error::Unknown("global client not set".to_string())); - } -} diff --git a/crates/http-std/src/reqwest_client.rs b/crates/http-std/src/reqwest_client.rs deleted file mode 100644 index b45551b8..00000000 --- a/crates/http-std/src/reqwest_client.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::{Client, FetchOptions, Method, Response, Result}; -use async_trait::async_trait; -use reqwest::header::HeaderMap; -use std::collections::HashMap; -use std::convert::TryFrom; - -pub struct ReqwestClient { - client: reqwest::Client, -} - -impl ReqwestClient { - pub fn new() -> Self { - ReqwestClient { - client: reqwest::Client::new(), - } - } -} - -#[async_trait] -impl Client for ReqwestClient { - async fn fetch(&self, url: &str, options: Option) -> Result { - let options = options.unwrap_or_default(); - let method = options.method.unwrap_or(Method::Get).to_string(); - - let mut req = match method.as_str() { - "GET" => self.client.get(url), - "POST" => self.client.post(url), - "PUT" => self.client.put(url), - _ => unreachable!(), - }; - - if let Some(headers) = options.headers { - let mut req_headers = HeaderMap::new(); - for (key, value) in headers { - req_headers.insert( - reqwest::header::HeaderName::try_from(key.as_str()).unwrap(), - value.parse().unwrap(), - ); - } - req = req.headers(req_headers); - } - - if let Some(body) = options.body { - req = req.body(body); - } - - let res = req.send().await.map_err(crate::Error::from)?; - - let status_code = res.status().as_u16(); - let mut headers = HashMap::new(); - for (key, value) in res.headers().iter() { - headers.insert(key.to_string(), value.to_str().unwrap().to_string()); - } - - let body = res.bytes().await.map_err(crate::Error::from)?.to_vec(); - - Ok(Response { - status_code, - headers, - body, - }) - } -} diff --git a/crates/web5/Cargo.toml b/crates/web5/Cargo.toml index 7c54383a..f5c65a76 100644 --- a/crates/web5/Cargo.toml +++ b/crates/web5/Cargo.toml @@ -8,6 +8,7 @@ license-file.workspace = true rust-version = "1.74.0" [dependencies] +async-trait = "0.1.83" base64 = { workspace = true } byteorder = "1.5.0" chrono = { workspace = true } @@ -32,8 +33,15 @@ x25519-dalek = { version = "2.0.1", features = ["getrandom", "static_secrets"] } zbase32 = "0.1.2" lazy_static = { workspace = true } flate2 = "1.0.33" -http-std = { path = "../http-std" } +reqwest = { version = "0.12", optional = true } +once_cell = "1.19.0" + +[features] +default = [] +http_reqwest = ["reqwest"] [dev-dependencies] mockito = "1.5.0" tokio = { version = "1.38.0", features = ["macros", "test-util"] } +web5 = { path = ".", features = ["http_reqwest"] } + diff --git a/crates/web5/src/credentials/create.rs b/crates/web5/src/credentials/create.rs index 5c824466..150c5a13 100644 --- a/crates/web5/src/credentials/create.rs +++ b/crates/web5/src/credentials/create.rs @@ -605,11 +605,11 @@ mod tests { .await; match result { - Err(Web5Error::Http(err)) => { + Err(Web5Error::Network(err)) => { assert!(err.to_string().contains("error sending request")) } _ => panic!( - "expected Web5Error::Http with specific message but got {:?}", + "expected Web5Error::Network with specific message but got {:?}", result ), }; @@ -636,11 +636,11 @@ mod tests { .await; match result { - Err(Web5Error::JsonSchema(err_msg)) => { - assert_eq!("failed to resolve status code 500", err_msg) + Err(Web5Error::Network(err_msg)) => { + assert!(err_msg.contains("Failed to fetch credential schema")) } _ => panic!( - "expected Web5Error::JsonSchema with specific message but got {:?}", + "expected Web5Error::Network with specific message but got {:?}", result ), } diff --git a/crates/web5/src/credentials/credential_schema.rs b/crates/web5/src/credentials/credential_schema.rs index 7af3159c..7ba3c5f0 100644 --- a/crates/web5/src/credentials/credential_schema.rs +++ b/crates/web5/src/credentials/credential_schema.rs @@ -1,5 +1,10 @@ +use std::collections::HashMap; + use super::verifiable_credential_1_1::VerifiableCredential; -use crate::errors::{Result, Web5Error}; +use crate::{ + errors::{Result, Web5Error}, + http::get_http_client, +}; use jsonschema::{Draft, JSONSchema}; use serde::{Deserialize, Serialize}; @@ -28,7 +33,22 @@ pub(crate) async fn validate_credential_schema( let url = &credential_schema.id; - let response = http_std::fetch(url, None).await?; + 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)) + .await + .map_err(|e| Web5Error::Network(format!("Failed to fetch credential schema: {}", e)))?; + + if !(200..300).contains(&response.status_code) { + return Err(Web5Error::Network(format!( + "Failed to fetch credential schema: non-successful response code {}", + response.status_code + ))); + } if !(200..300).contains(&response.status_code) { return Err(Web5Error::JsonSchema(format!( diff --git a/crates/web5/src/credentials/verifiable_credential_1_1.rs b/crates/web5/src/credentials/verifiable_credential_1_1.rs index 9fd01e5e..f4795053 100644 --- a/crates/web5/src/credentials/verifiable_credential_1_1.rs +++ b/crates/web5/src/credentials/verifiable_credential_1_1.rs @@ -813,11 +813,11 @@ mod tests { let result = VerifiableCredential::from_vc_jwt(vc_jwt_with_invalid_url, true).await; match result { - Err(Web5Error::Http(err)) => { + Err(Web5Error::Network(err)) => { assert!(err.to_string().contains("error sending request")) } _ => panic!( - "expected Web5Error::Http with specific message but got {:?}", + "expected Web5Error::Network with specific message but got {:?}", result ), }; @@ -839,11 +839,11 @@ mod tests { let result = VerifiableCredential::from_vc_jwt(vc_jwt_at_port, true).await; match result { - Err(Web5Error::JsonSchema(err_msg)) => { - assert_eq!("failed to resolve status code 500", err_msg) + Err(Web5Error::Network(err_msg)) => { + assert!(err_msg.contains("Failed to fetch credential schema")) } _ => panic!( - "expected Web5Error::JsonSchema with specific message but got {:?}", + "expected Web5Error::Network with specific message but got {:?}", result ), } diff --git a/crates/web5/src/dids/methods/did_dht/mod.rs b/crates/web5/src/dids/methods/did_dht/mod.rs index 773e8c44..b224a603 100644 --- a/crates/web5/src/dids/methods/did_dht/mod.rs +++ b/crates/web5/src/dids/methods/did_dht/mod.rs @@ -18,8 +18,9 @@ use crate::{ }, }, errors::{Result, Web5Error}, + http::get_http_client, }; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; mod bep44; mod document_packet; @@ -190,25 +191,20 @@ impl DidDht { bearer_did.did.id.trim_start_matches('/') ); - let response = http_std::fetch( - &url, - Some(http_std::FetchOptions { - method: Some(http_std::Method::Put), - headers: Some( - [ - ( - "Content-Type".to_string(), - "application/octet-stream".to_string(), - ), - ("Content-Length".to_string(), body.len().to_string()), - ] - .into_iter() - .collect(), - ), - body: Some(body), - }), - ) - .await?; + 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) + .await + .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(), @@ -265,8 +261,14 @@ impl DidDht { did.id.trim_start_matches('/') ); - let response = http_std::fetch(&url, None) - .await // todo here + 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)) + .await .map_err(|_| ResolutionMetadataError::InternalError)?; if response.status_code == 404 { diff --git a/crates/web5/src/dids/methods/did_web/resolver.rs b/crates/web5/src/dids/methods/did_web/resolver.rs index 76dc1bee..fbc90034 100644 --- a/crates/web5/src/dids/methods/did_web/resolver.rs +++ b/crates/web5/src/dids/methods/did_web/resolver.rs @@ -1,9 +1,14 @@ -use crate::dids::{ - data_model::document::Document, - did::Did, - resolution::{ - resolution_metadata::ResolutionMetadataError, resolution_result::ResolutionResult, +use std::collections::HashMap; + +use crate::{ + dids::{ + data_model::document::Document, + did::Did, + resolution::{ + resolution_metadata::ResolutionMetadataError, resolution_result::ResolutionResult, + }, }, + http::get_http_client, }; use url::Url; @@ -43,10 +48,20 @@ impl Resolver { } pub async fn resolve(&self) -> Result { - let response = http_std::fetch(&self.http_url, None) + 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(&self.http_url, Some(headers)) .await .map_err(|_| ResolutionMetadataError::InternalError)?; + if !(200..300).contains(&response.status_code) { + return Err(ResolutionMetadataError::InternalError); + } + if response.status_code == 404 { return Err(ResolutionMetadataError::NotFound); } else if !(200..300).contains(&response.status_code) { diff --git a/crates/web5/src/errors.rs b/crates/web5/src/errors.rs index 05b4aa4e..e84803fa 100644 --- a/crates/web5/src/errors.rs +++ b/crates/web5/src/errors.rs @@ -2,7 +2,6 @@ use crate::{ credentials::VerificationError, dids::resolution::resolution_metadata::ResolutionMetadataError, }; use base64::DecodeError; -use http_std::Error as HttpError; use serde_json::Error as SerdeJsonError; use std::sync::PoisonError; @@ -30,9 +29,9 @@ pub enum Web5Error { Network(String), #[error("datetime error {0}")] DateTime(String), + #[error("http error {0}")] + Http(String), - #[error(transparent)] - Http(#[from] HttpError), #[error(transparent)] Resolution(#[from] ResolutionMetadataError), #[error(transparent)] diff --git a/crates/web5/src/http.rs b/crates/web5/src/http.rs new file mode 100644 index 00000000..3602dfd6 --- /dev/null +++ b/crates/web5/src/http.rs @@ -0,0 +1,222 @@ +use async_trait::async_trait; +use once_cell::sync::OnceCell; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, fmt, str::FromStr, sync::Arc}; + +use crate::errors::Web5Error; + +#[async_trait] +pub trait HttpClient: Send + Sync { + async fn fetch( + &self, + url: &str, + options: Option, + ) -> std::result::Result>; + + async fn get( + &self, + url: &str, + headers: Option>, + ) -> std::result::Result> { + self.fetch( + url, + Some(FetchOptions { + method: Some(Method::Get), + headers, + body: None, + }), + ) + .await + } + + async fn post( + &self, + url: &str, + headers: Option>, + body: &[u8], + ) -> std::result::Result> { + self.fetch( + url, + Some(FetchOptions { + method: Some(Method::Post), + headers, + body: Some(body.to_vec()), + }), + ) + .await + } + + async fn put( + &self, + url: &str, + headers: Option>, + body: &[u8], + ) -> std::result::Result> { + self.fetch( + url, + Some(FetchOptions { + method: Some(Method::Put), + headers, + body: Some(body.to_vec()), + }), + ) + .await + } +} + +#[derive(Default, Serialize, Deserialize)] +pub struct FetchOptions { + pub method: Option, + pub headers: Option>, + pub body: Option>, +} + +#[derive(Serialize, Deserialize)] +pub enum Method { + Get, + Post, + Put, +} + +pub struct HttpResponse { + pub status_code: u16, + pub headers: HashMap, + pub body: Vec, +} + +impl fmt::Display for Method { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let method_str = match self { + Method::Get => "GET", + Method::Post => "POST", + Method::Put => "PUT", + }; + write!(f, "{}", method_str) + } +} + +impl FromStr for Method { + type Err = Web5Error; + + fn from_str(s: &str) -> crate::errors::Result { + match s.to_ascii_uppercase().as_ref() { + "GET" => Ok(Method::Get), + "POST" => Ok(Method::Post), + "PUT" => Ok(Method::Put), + _ => Err(Web5Error::Parameter(format!("unknown method {}", s))), + } + } +} + +static HTTP_CLIENT: OnceCell> = OnceCell::new(); + +#[cfg(feature = "http_reqwest")] +pub fn get_http_client() -> &'static dyn HttpClient { + HTTP_CLIENT + .get_or_init(|| Arc::new(reqwest_http_client::ReqwestHttpClient::new())) + .as_ref() +} + +#[cfg(not(feature = "http_reqwest"))] +pub fn get_http_client() -> &'static dyn HttpClient { + HTTP_CLIENT + .get() + .expect("HttpClient has not been set. Please call set_http_client().") + .as_ref() +} + +#[cfg(feature = "http_reqwest")] +pub fn set_http_client(_: Arc) { + panic!("Cannot set a custom HttpClient when the reqwest feature is enabled."); +} + +#[cfg(not(feature = "http_reqwest"))] +pub fn set_http_client(client: Arc) { + HTTP_CLIENT + .set(client) + .unwrap_or_else(|_| panic!("HttpClient has already been set.")); +} + +#[cfg(feature = "http_reqwest")] +mod reqwest_http_client { + use super::*; + use reqwest::{Client as ReqwestClient, Method as ReqwestMethod, Response as ReqwestResponse}; + use std::error::Error; + + pub struct ReqwestHttpClient { + client: ReqwestClient, + } + + impl ReqwestHttpClient { + pub fn new() -> Self { + ReqwestHttpClient { + client: ReqwestClient::new(), + } + } + + fn map_method(method: Option) -> ReqwestMethod { + match method { + Some(Method::Post) => ReqwestMethod::POST, + Some(Method::Put) => ReqwestMethod::PUT, + _ => ReqwestMethod::GET, + } + } + + async fn build_request( + &self, + url: &str, + options: Option, + ) -> Result> { + let FetchOptions { + method, + headers, + body, + } = options.unwrap_or_default(); + + let req_method = Self::map_method(method); + let mut req = self.client.request(req_method, url); + + if let Some(headers) = headers { + for (key, value) in headers { + req = req.header(&key, &value); + } + } + + if let Some(body) = body { + req = req.body(body); + } + + Ok(req) + } + + async fn parse_response(response: ReqwestResponse) -> Result> { + 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().await?.to_vec(); + + Ok(HttpResponse { + status_code, + headers, + body, + }) + } + } + + #[async_trait] + impl HttpClient for ReqwestHttpClient { + async fn fetch( + &self, + url: &str, + options: Option, + ) -> Result> { + let req = self.build_request(url, options).await?; + let res = req.send().await?; + Self::parse_response(res).await + } + } +} diff --git a/crates/web5/src/lib.rs b/crates/web5/src/lib.rs index c11653e8..103c9026 100644 --- a/crates/web5/src/lib.rs +++ b/crates/web5/src/lib.rs @@ -4,8 +4,25 @@ pub mod dids; mod datetime; pub mod errors; +mod http; pub mod jose; pub mod json; +pub use http::set_http_client; + #[cfg(test)] mod test_vectors; +#[cfg(test)] +mod tests { + #[cfg(feature = "http_reqwest")] + #[test] + fn test_with_reqwest_feature() { + println!("http_reqwest feature is enabled!"); + } + + #[cfg(not(feature = "http_reqwest"))] + #[test] + fn test_without_reqwest_feature() { + println!("http_reqwest feature is NOT enabled!"); + } +} diff --git a/crates/web5_cli/build/x86_64_unknown_linux_gnu/Dockerfile b/crates/web5_cli/build/x86_64_unknown_linux_gnu/Dockerfile index 18909259..c4ee155a 100644 --- a/crates/web5_cli/build/x86_64_unknown_linux_gnu/Dockerfile +++ b/crates/web5_cli/build/x86_64_unknown_linux_gnu/Dockerfile @@ -19,7 +19,6 @@ COPY bindings/web5_c ./bindings/web5_c COPY bindings/web5_uniffi_wrapper ./bindings/web5_uniffi_wrapper COPY bindings/web5_uniffi ./bindings/web5_uniffi COPY bindings/web5_wasm ./bindings/web5_wasm -COPY crates/http-std ./crates/http-std COPY crates/web5 ./crates/web5 COPY crates/web5_cli ./crates/web5_cli diff --git a/crates/web5_cli/build/x86_64_unknown_linux_musl/Dockerfile b/crates/web5_cli/build/x86_64_unknown_linux_musl/Dockerfile index 235b6c41..aaa61bc0 100644 --- a/crates/web5_cli/build/x86_64_unknown_linux_musl/Dockerfile +++ b/crates/web5_cli/build/x86_64_unknown_linux_musl/Dockerfile @@ -36,7 +36,6 @@ COPY bindings/web5_c ./bindings/web5_c COPY bindings/web5_uniffi_wrapper ./bindings/web5_uniffi_wrapper COPY bindings/web5_uniffi ./bindings/web5_uniffi COPY bindings/web5_wasm ./bindings/web5_wasm -COPY crates/http-std ./crates/http-std COPY crates/web5 ./crates/web5 COPY crates/web5_cli ./crates/web5_cli