From bf0209b7303a2231fa44899dad4d8bb1e6932aaa Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Mon, 23 Sep 2024 16:53:53 -0700 Subject: [PATCH] Revert Revert Remove reqwest dependency (#366) --- .../x86_64_unknown_linux_musl/Dockerfile | 14 +- crates/web5/Cargo.toml | 3 +- crates/web5/src/credentials/create.rs | 16 +- .../web5/src/credentials/credential_schema.rs | 25 +- .../credentials/verifiable_credential_1_1.rs | 16 +- crates/web5/src/dids/bearer_did.rs | 6 +- crates/web5/src/dids/did.rs | 1 - crates/web5/src/dids/methods/did_dht/mod.rs | 44 +--- crates/web5/src/dids/methods/did_jwk.rs | 1 - .../web5/src/dids/methods/did_web/resolver.rs | 46 ++-- crates/web5/src/dids/portable_did.rs | 1 - crates/web5/src/errors.rs | 2 + crates/web5/src/http.rs | 220 ++++++++++++++++++ crates/web5/src/lib.rs | 1 + .../x86_64_unknown_linux_musl/Dockerfile | 12 +- 15 files changed, 280 insertions(+), 128 deletions(-) create mode 100644 crates/web5/src/http.rs 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 3c8708e5..51552785 100644 --- a/bindings/web5_uniffi/libtargets/x86_64_unknown_linux_musl/Dockerfile +++ b/bindings/web5_uniffi/libtargets/x86_64_unknown_linux_musl/Dockerfile @@ -4,7 +4,6 @@ FROM --platform=linux/amd64 alpine:latest RUN apk add --no-cache \ build-base \ musl-dev \ - openssl-dev \ linux-headers \ rustup \ libgcc \ @@ -13,21 +12,12 @@ RUN apk add --no-cache \ git \ perl \ make \ - bash \ - openssl-libs-static + bash # Install rust RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | bash -s -- -y --no-modify-path --default-toolchain 1.74.0 ENV PATH="/root/.cargo/bin:${PATH}" -# Set environment variables to ensure vendored OpenSSL is used -ENV OPENSSL_STATIC=1 -ENV OPENSSL_LIB_DIR=/usr/lib -ENV OPENSSL_INCLUDE_DIR=/usr/include -ENV PKG_CONFIG_ALLOW_CROSS=1 -ENV PKG_CONFIG_PATH=/usr/lib/pkgconfig -ENV LIBRARY_PATH="/usr/lib:/usr/local/lib" - # Copy the source code to the container WORKDIR /usr/src/myapp COPY Cargo.toml ./ @@ -44,7 +34,7 @@ RUN cargo build --release --package web5_uniffi # Compile as a dynamic lib (.so) from our static lib (.a) while keeping dependencies self-contained RUN gcc -shared -o target/release/libweb5_uniffi.so -Wl,--whole-archive \ target/release/libweb5_uniffi.a -Wl,--no-whole-archive -static-libgcc \ - -L/usr/lib -lssl -lcrypto -Wl,-Bdynamic -fPIC + -Wl,-Bdynamic -fPIC # Set the entrypoint, so that we can `docker cp` the build output CMD tail -f /dev/null diff --git a/crates/web5/Cargo.toml b/crates/web5/Cargo.toml index f929840c..b4468953 100644 --- a/crates/web5/Cargo.toml +++ b/crates/web5/Cargo.toml @@ -18,7 +18,6 @@ k256 = { version = "0.13.3", features = ["ecdsa", "jwk"] } tokio = "1.38.0" rand = { workspace = true } regex = "1.10.4" -reqwest = { version = "0.12.4", features = ["json", "blocking"] } serde = { workspace = true } serde_json = { workspace = true } sha2 = "0.10.8" @@ -30,6 +29,8 @@ x25519-dalek = { version = "2.0.1", features = ["getrandom", "static_secrets"] } zbase32 = "0.1.2" 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" [dev-dependencies] mockito = "1.5.0" diff --git a/crates/web5/src/credentials/create.rs b/crates/web5/src/credentials/create.rs index f170db37..9fae7ab5 100644 --- a/crates/web5/src/credentials/create.rs +++ b/crates/web5/src/credentials/create.rs @@ -558,7 +558,7 @@ mod tests { #[test] fn test_schema_resolve_network_issue() { - let url = "invalid url".to_string(); // here + let url = "http://local".to_string(); // here let result = create_vc( issuer(), @@ -571,7 +571,11 @@ mod tests { match result { Err(Web5Error::Network(err_msg)) => { - assert!(err_msg.contains("unable to resolve json schema")) + assert!( + err_msg.contains("failed to connect to host"), + "Error message is: {}", + err_msg + ) } _ => panic!( "expected Web5Error::Network with specific message but got {:?}", @@ -600,8 +604,8 @@ mod tests { ); match result { - Err(Web5Error::JsonSchema(err_msg)) => { - assert!(err_msg.contains("non-200 response when resolving json schema")) + Err(Web5Error::Http(err_msg)) => { + assert_eq!("non-successful response code 500", err_msg) } _ => panic!( "expected Web5Error::JsonSchema with specific message but got {:?}", @@ -632,8 +636,8 @@ mod tests { ); match result { - Err(Web5Error::JsonSchema(err_msg)) => { - assert!(err_msg.contains("unable to parse json schema from response body")) + Err(Web5Error::Http(err_msg)) => { + assert!(err_msg.contains("unable to parse json response body")) } _ => panic!( "expected Web5Error::JsonSchema with specific message but got {:?}", diff --git a/crates/web5/src/credentials/credential_schema.rs b/crates/web5/src/credentials/credential_schema.rs index 3efeb3ba..56ab9898 100644 --- a/crates/web5/src/credentials/credential_schema.rs +++ b/crates/web5/src/credentials/credential_schema.rs @@ -1,7 +1,9 @@ use super::verifiable_credential_1_1::VerifiableCredential; -use crate::errors::{Result, Web5Error}; +use crate::{ + errors::{Result, Web5Error}, + http::get_json, +}; use jsonschema::{Draft, JSONSchema}; -use reqwest::blocking::get; use serde::{Deserialize, Serialize}; pub const CREDENTIAL_SCHEMA_TYPE: &str = "JsonSchema"; @@ -28,23 +30,8 @@ pub(crate) fn validate_credential_schema( } let url = &credential_schema.id; - let response = get(url).map_err(|err| { - Web5Error::Network(format!("unable to resolve json schema {} {}", url, err)) - })?; - if !response.status().is_success() { - return Err(Web5Error::JsonSchema(format!( - "non-200 response when resolving json schema {} {}", - url, - response.status() - ))); - } - let schema_json = response.json::().map_err(|err| { - Web5Error::JsonSchema(format!( - "unable to parse json schema from response body {} {}", - url, err - )) - })?; - let compiled_schema = JSONSchema::options().compile(&schema_json).map_err(|err| { + let json_schema = get_json::(url)?; + 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/credentials/verifiable_credential_1_1.rs b/crates/web5/src/credentials/verifiable_credential_1_1.rs index 84671ff7..50ac1c9d 100644 --- a/crates/web5/src/credentials/verifiable_credential_1_1.rs +++ b/crates/web5/src/credentials/verifiable_credential_1_1.rs @@ -791,13 +791,17 @@ mod tests { #[test] fn test_schema_resolve_network_issue() { - let vc_jwt_with_invalid_url = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSmZYelYxVEU1bWNVWTRRbTB6ZVhnMmJVRndMVlJJV25sSk5WcDJWQzFmYVVKbExWZDJiMHRuTTFwakluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOmRlNDY2N2YxLTMzM2ItNDg4OC1hMDc5LTdkMGU1N2JiZmFlZiIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKZlh6VjFURTVtY1VZNFFtMHplWGcyYlVGd0xWUklXbmxKTlZwMlZDMWZhVUpsTFZkMmIwdG5NMXBqSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0zMFQxNTowNToyMC43NjQ0MDgrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6ImludmFsaWQgdXJsIiwidHlwZSI6Ikpzb25TY2hlbWEifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSmZYelYxVEU1bWNVWTRRbTB6ZVhnMmJVRndMVlJJV25sSk5WcDJWQzFmYVVKbExWZDJiMHRuTTFwakluMCIsImp0aSI6InVybjp1dWlkOmRlNDY2N2YxLTMzM2ItNDg4OC1hMDc5LTdkMGU1N2JiZmFlZiIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTAzMDMyMCwiaWF0IjoxNzI1MDMwMzIwfQ.3sH7qzI7QrQMdkWIvqf7k8Mr2dMGjWBLrv4QB8gEz0t83RSFMtG-fWT-YVkUlo1qMvC4gNjT2Jc0eObCAA7VDQ"#; + let vc_jwt_with_invalid_url = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZDI1NTE5Iiwia2lkIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKTmEycDVaRlo1ZFhaU1psaExRMDVWYm0wNVVWRnJVbkUwY0doWVdYRTBObUpFVjJGemFHOW5kbXhWSW4wIzAifQ.eyJpc3MiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2lZM0oySWpvaVJXUXlOVFV4T1NJc0luZ2lPaUpOYTJwNVpGWjVkWFpTWmxoTFEwNVZibTA1VVZGclVuRTBjR2hZV1hFME5tSkVWMkZ6YUc5bmRteFZJbjAiLCJqdGkiOiJ1cm46dXVpZDo2YzM2YzU0Zi02M2VhLTRiY2MtOTgxOS0zYmNmMGIyYmUxMDgiLCJzdWIiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkiLCJuYmYiOjE3MjYwODk0NDIsImlhdCI6MTcyNjA4OTQ0MiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6Imh0dHA6Ly9sb2NhbC9zY2hlbWFzL2VtYWlsLmpzb24iLCJ0eXBlIjoiSnNvblNjaGVtYSJ9LCJpZCI6InVybjp1dWlkOjZjMzZjNTRmLTYzZWEtNGJjYy05ODE5LTNiY2YwYjJiZTEwOCIsImlzc3VlciI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSk5hMnA1WkZaNWRYWlNabGhMUTA1VmJtMDVVVkZyVW5FMGNHaFlXWEUwTm1KRVYyRnphRzluZG14VkluMCIsImlzc3VhbmNlRGF0ZSI6IjIwMjQtMDktMTFUMjE6MTc6MjJaIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSJ9fX0.eZfQZdkDB2D2QMs6BPaxjU-FCJLIGMlCz0sF5FjhHkaizItfv3zGXqWVEjc8f-SRiLSmujlEKgwfw22cCvnDAQ"#; let result = VerifiableCredential::from_vc_jwt(vc_jwt_with_invalid_url, true); match result { Err(Web5Error::Network(err_msg)) => { - assert!(err_msg.contains("unable to resolve json schema")) + assert!( + err_msg.contains("failed to connect to host"), + "Error message is: {}", + err_msg + ) } _ => panic!( "expected Web5Error::Network with specific message but got {:?}", @@ -822,8 +826,8 @@ mod tests { let result = VerifiableCredential::from_vc_jwt(vc_jwt_at_port, true); match result { - Err(Web5Error::JsonSchema(err_msg)) => { - assert!(err_msg.contains("non-200 response when resolving json schema")) + Err(Web5Error::Http(err_msg)) => { + assert_eq!("non-successful response code 500", err_msg) } _ => panic!( "expected Web5Error::JsonSchema with specific message but got {:?}", @@ -850,8 +854,8 @@ mod tests { let result = VerifiableCredential::from_vc_jwt(vc_jwt_at_port, true); match result { - Err(Web5Error::JsonSchema(err_msg)) => { - assert!(err_msg.contains("unable to parse json schema from response body")) + Err(Web5Error::Http(err_msg)) => { + assert!(err_msg.contains("unable to parse json response body")) } _ => panic!( "expected Web5Error::JsonSchema with specific message but got {:?}", diff --git a/crates/web5/src/dids/bearer_did.rs b/crates/web5/src/dids/bearer_did.rs index cb92c6ad..a7d57797 100644 --- a/crates/web5/src/dids/bearer_did.rs +++ b/crates/web5/src/dids/bearer_did.rs @@ -147,7 +147,7 @@ mod tests { key_manager: Some(key_manager.clone()), ..Default::default() })) - .unwrap(); + .unwrap(); let portable_did = PortableDid { did_uri: did_jwk_bearer_did.did.uri, @@ -208,7 +208,7 @@ mod tests { key_manager: Some(key_manager.clone()), ..Default::default() })) - .unwrap(); + .unwrap(); let result = bearer_did.to_portable_did(key_manager); assert!(result.is_ok()); @@ -216,4 +216,4 @@ mod tests { assert_eq!(bearer_did.did.uri, portable_did.did_uri); } } -} \ No newline at end of file +} diff --git a/crates/web5/src/dids/did.rs b/crates/web5/src/dids/did.rs index f9df89b9..dd71d0d5 100644 --- a/crates/web5/src/dids/did.rs +++ b/crates/web5/src/dids/did.rs @@ -74,7 +74,6 @@ lazy_static! { } impl Did { - /// Parses a given DID URI into a `Did` struct. /// /// This function extracts and parses components from a DID URI, including the method, diff --git a/crates/web5/src/dids/methods/did_dht/mod.rs b/crates/web5/src/dids/methods/did_dht/mod.rs index 291d7ee7..ca910544 100644 --- a/crates/web5/src/dids/methods/did_dht/mod.rs +++ b/crates/web5/src/dids/methods/did_dht/mod.rs @@ -1,5 +1,4 @@ use bep44::Bep44Message; -use reqwest::blocking::Client; use simple_dns::Packet; use crate::{ @@ -19,6 +18,7 @@ use crate::{ }, }, errors::{Result, Web5Error}, + http::{get, put}, }; use std::sync::Arc; @@ -191,15 +191,8 @@ impl DidDht { bearer_did.did.id.trim_start_matches('/') ); - let client = Client::new(); - let response = client - .put(url) - .header("Content-Type", "application/octet-stream") - .body(body) - .send() - .map_err(|_| Web5Error::Network("failed to publish DID to mainline".to_string()))?; - - if response.status() != 200 { + let response = put(&url, &body)?; + if response.status_code != 200 { return Err(Web5Error::Network( "failed to PUT DID to mainline".to_string(), )); @@ -255,35 +248,16 @@ impl DidDht { did.id.trim_start_matches('/') ); - let client = Client::new(); - - // Make the GET request - let response = client - .get(url) - .send() - .map_err(|_| ResolutionMetadataError::InternalError)?; - - // Check if the status is not 200 - let status = response.status(); - if status == 404 { - return Err(ResolutionMetadataError::NotFound)?; - } else if status != 200 { - return Err(ResolutionMetadataError::InternalError)?; - } - - // check http response status is 200 and body is nonempty - let body = response - .bytes() - .map_err(|_| ResolutionMetadataError::NotFound)?; + let response = get(&url).map_err(|_| ResolutionMetadataError::InternalError)?; - // Check if the body is empty - if body.is_empty() { - return Err(ResolutionMetadataError::NotFound)?; + if response.status_code == 404 { + return Err(ResolutionMetadataError::NotFound); + } else if response.status_code != 200 { + return Err(ResolutionMetadataError::InternalError); } // bep44 decode and verify response body bytes - let body: Vec = body.into(); - let bep44_message = Bep44Message::decode(&body) + let bep44_message = Bep44Message::decode(&response.body) .map_err(|_| ResolutionMetadataError::InvalidDidDocument)?; bep44_message .verify(&Ed25519Verifier::new(identity_key)) diff --git a/crates/web5/src/dids/methods/did_jwk.rs b/crates/web5/src/dids/methods/did_jwk.rs index 9ec58857..bbc85968 100644 --- a/crates/web5/src/dids/methods/did_jwk.rs +++ b/crates/web5/src/dids/methods/did_jwk.rs @@ -31,7 +31,6 @@ pub struct DidJwk; /// by third parties without relying on a separate blockchain or ledger. This is particularly useful for scenarios /// involving verifiable credentials or capabilities. impl DidJwk { - /// Creates a new "did:jwk" DID, derived from a public key. /// /// This method generates a "did:jwk" DID by creating a key pair, using the provided key manager, and diff --git a/crates/web5/src/dids/methods/did_web/resolver.rs b/crates/web5/src/dids/methods/did_web/resolver.rs index ad0e8efb..2ab57612 100644 --- a/crates/web5/src/dids/methods/did_web/resolver.rs +++ b/crates/web5/src/dids/methods/did_web/resolver.rs @@ -1,11 +1,13 @@ -use crate::dids::{ - data_model::document::Document, - did::Did, - resolution::{ - resolution_metadata::ResolutionMetadataError, resolution_result::ResolutionResult, +use crate::{ + dids::{ + data_model::document::Document, + did::Did, + resolution::{ + resolution_metadata::ResolutionMetadataError, resolution_result::ResolutionResult, + }, }, + http::get_json, }; -use reqwest::header::HeaderMap; use std::{ future::{Future, IntoFuture}, pin::Pin, @@ -48,32 +50,12 @@ impl Resolver { } async fn resolve(url: String) -> Result { - let headers = HeaderMap::new(); - - let client = reqwest::Client::builder() - .default_headers(headers) - .build() - .map_err(|_| ResolutionMetadataError::InternalError)?; - - let response = client - .get(&url) - .send() - .await - .map_err(|_| ResolutionMetadataError::InternalError)?; - - if response.status().is_success() { - let did_document = response - .json::() - .await - .map_err(|_| ResolutionMetadataError::RepresentationNotSupported)?; - - Ok(ResolutionResult { - document: Some(did_document), - ..Default::default() - }) - } else { - Err(ResolutionMetadataError::NotFound) - } + let document = + get_json::(&url).map_err(|_| ResolutionMetadataError::InternalError)?; + Ok(ResolutionResult { + document: Some(document), + ..Default::default() + }) } } diff --git a/crates/web5/src/dids/portable_did.rs b/crates/web5/src/dids/portable_did.rs index 67a73647..40ac05e2 100644 --- a/crates/web5/src/dids/portable_did.rs +++ b/crates/web5/src/dids/portable_did.rs @@ -5,7 +5,6 @@ use crate::{ }; use serde::{Deserialize, Serialize}; - /// Represents a Portable DID (Decentralized Identifier) that includes the DID Document and /// its associated private keys. This structure is useful for exporting/importing DIDs /// across different contexts or process boundaries. diff --git a/crates/web5/src/errors.rs b/crates/web5/src/errors.rs index 157a3263..e84803fa 100644 --- a/crates/web5/src/errors.rs +++ b/crates/web5/src/errors.rs @@ -29,6 +29,8 @@ pub enum Web5Error { Network(String), #[error("datetime error {0}")] DateTime(String), + #[error("http error {0}")] + Http(String), #[error(transparent)] Resolution(#[from] ResolutionMetadataError), diff --git a/crates/web5/src/http.rs b/crates/web5/src/http.rs new file mode 100644 index 00000000..ab7c627b --- /dev/null +++ b/crates/web5/src/http.rs @@ -0,0 +1,220 @@ +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; + +pub struct HttpResponse { + pub status_code: u16, + #[allow(dead_code)] + pub headers: HashMap, + 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)))?; + + 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, + }) +} + +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) +} + +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]; + + // Convert the header part to a string (since headers are ASCII/UTF-8 compliant) + let header_str = String::from_utf8_lossy(header_part); + + // 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()))?; + + 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())); + } + + 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()); + } + } + + // The body is the part after the \r\n\r\n separator + let body = response_bytes[header_end + 4..].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 + ))); + } + + 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 dc2eb8f3..1806cd43 100644 --- a/crates/web5/src/lib.rs +++ b/crates/web5/src/lib.rs @@ -4,6 +4,7 @@ pub mod dids; mod datetime; pub mod errors; +mod http; mod jose; pub mod json; 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 d08797c6..55ffaff3 100644 --- a/crates/web5_cli/build/x86_64_unknown_linux_musl/Dockerfile +++ b/crates/web5_cli/build/x86_64_unknown_linux_musl/Dockerfile @@ -4,7 +4,6 @@ FROM --platform=linux/amd64 alpine:latest RUN apk add --no-cache \ build-base \ musl-dev \ - openssl-dev \ linux-headers \ rustup \ libgcc \ @@ -13,21 +12,12 @@ RUN apk add --no-cache \ git \ perl \ make \ - bash \ - openssl-libs-static + bash # Install rust RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | bash -s -- -y --no-modify-path --default-toolchain 1.74.0 ENV PATH="/root/.cargo/bin:${PATH}" -# Set environment variables to ensure vendored OpenSSL is used -ENV OPENSSL_STATIC=1 -ENV OPENSSL_LIB_DIR=/usr/lib -ENV OPENSSL_INCLUDE_DIR=/usr/include -ENV PKG_CONFIG_ALLOW_CROSS=1 -ENV PKG_CONFIG_PATH=/usr/lib/pkgconfig -ENV LIBRARY_PATH="/usr/lib:/usr/local/lib" - # Copy the source code to the container WORKDIR /usr/src/myapp COPY Cargo.toml ./