Skip to content

Commit

Permalink
Add http-std
Browse files Browse the repository at this point in the history
  • Loading branch information
KendallWeihe committed Sep 24, 2024
1 parent ed7f439 commit a3ba3ea
Show file tree
Hide file tree
Showing 15 changed files with 333 additions and 402 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
members = [
"bindings/web5_uniffi",
"bindings/web5_uniffi_wrapper",
"bindings/web5_wasm",
"bindings/web5_wasm", "crates/http-std",
"crates/web5",
"crates/web5_cli",
]
Expand All @@ -17,6 +17,7 @@ license-file = "LICENSE"
[workspace.dependencies]
base64 = "0.22.0"
chrono = { version = "0.4.37", features = ["std"] }
lazy_static = "1.5.0"
thiserror = "1.0.50"
rand = "0.8.5"
serde = { version = "1.0.193", features = ["derive"] }
Expand Down
14 changes: 14 additions & 0 deletions crates/http-std/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "http-std"
version = "0.1.0"
edition = "2021"
homepage.workspace = true
repository.workspace = true
license-file.workspace = true

[dependencies]
lazy_static = { workspace = true }
rustls = { version = "0.23.13", default-features = false, features = ["std", "tls12"] }
rustls-native-certs = "0.8.0"
thiserror = { workspace = true }
url = "2.5.0"
35 changes: 35 additions & 0 deletions crates/http-std/src/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use crate::Result;
use std::collections::HashMap;

pub trait Client: Send + Sync {
fn fetch(&self, url: &str, options: Option<FetchOptions>) -> Result<Response>;
}

#[derive(Default)]
pub struct FetchOptions {
pub method: Option<Method>,
pub headers: Option<HashMap<String, String>>,
pub body: Option<Vec<u8>>,
}

pub struct Response {
pub status_code: u16,
pub headers: HashMap<String, String>,
pub body: Vec<u8>,
}

pub enum Method {
Get,
Post,
Put,
}

impl ToString for Method {
fn to_string(&self) -> String {
match self {
Method::Get => "GET".to_string(),
Method::Post => "POST".to_string(),
Method::Put => "PUT".to_string(),
}
}
}
169 changes: 169 additions & 0 deletions crates/http-std/src/default_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
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 url::Url;

use crate::{Client, Error, FetchOptions, Method, Response, Result};

struct Destination {
pub host: String,
pub path: String,
pub port: u16,
pub schema: String,
}

fn parse_destination(url: &str) -> Result<Destination> {
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<Vec<u8>> {
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); // Arc allows sharing the 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
.write_all(request)
.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
.write_all(request)
.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<Response> {
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::<u16>()
.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;

impl Client for DefaultClient {
fn fetch(&self, url: &str, options: Option<FetchOptions>) -> Result<Response> {
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.to_string(),
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)
}
}
13 changes: 13 additions & 0 deletions crates/http-std/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#[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),
}

pub type Result<T> = std::result::Result<T, Error>;
28 changes: 28 additions & 0 deletions crates/http-std/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
mod client;
mod default_client;
mod error;

use lazy_static::lazy_static;
use std::sync::{Arc, Mutex};

pub use client::{Client, FetchOptions, Method, Response};
pub use error::{Error, Result};

lazy_static! {
static ref CLIENT: Mutex<Arc<dyn Client>> = Mutex::new(Arc::new(default_client::DefaultClient));
}

pub fn set_client(client: Arc<dyn Client>) {
let mut global_client = CLIENT.lock().unwrap();
*global_client = client;
}

pub fn get_client() -> Arc<dyn Client> {
let client = CLIENT.lock().unwrap();
client.clone()
}

pub fn fetch(url: &str, options: Option<FetchOptions>) -> Result<Response> {
let client = get_client();
client.fetch(url, options)
}
7 changes: 2 additions & 5 deletions crates/web5/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,9 @@ url = "2.5.0"
uuid = { workspace = true }
x25519-dalek = { version = "2.0.1", features = ["getrandom", "static_secrets"] }
zbase32 = "0.1.2"
lazy_static = "1.5.0"
lazy_static = { workspace = true }
flate2 = "1.0.33"
rustls = { version = "0.23.13", default-features = false, features = ["std", "tls12"] }
rustls-native-certs = "0.8.0"
# TODO temporary
reqwest = { version = "0.12.4", features = ["json", "blocking"] }
http-std = { path = "../http-std" }

[dev-dependencies]
mockito = "1.5.0"
Expand Down
18 changes: 9 additions & 9 deletions crates/web5/src/credentials/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -570,14 +570,14 @@ mod tests {
);

match result {
Err(Web5Error::Http(err_msg)) => {
assert!(err_msg.contains("get request failed"))
Err(Web5Error::Http(err)) => {
assert!(err.to_string().contains("failed to connect to host"))
}
_ => panic!(
"expected Web5Error::Http with specific message but got {:?}",
result
),
}
};
}

#[test]
Expand All @@ -600,11 +600,11 @@ mod tests {
);

match result {
Err(Web5Error::Http(err_msg)) => {
assert!(err_msg.contains("http error status code 500"))
Err(Web5Error::JsonSchema(err_msg)) => {
assert_eq!("failed to resolve status code 500", err_msg)
}
_ => panic!(
"expected Web5Error::Http with specific message but got {:?}",
"expected Web5Error::JsonSchema with specific message but got {:?}",
result
),
}
Expand Down Expand Up @@ -632,11 +632,11 @@ mod tests {
);

match result {
Err(Web5Error::Http(err_msg)) => {
assert!(err_msg.contains("failed to parse json"))
Err(Web5Error::Json(err_msg)) => {
assert!(err_msg.contains("expected value at line"))
}
_ => panic!(
"expected Web5Error::Http with specific message but got {:?}",
"expected Web5Error::Json with specific message but got {:?}",
result
),
}
Expand Down
18 changes: 13 additions & 5 deletions crates/web5/src/credentials/credential_schema.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
use super::verifiable_credential_1_1::VerifiableCredential;
use crate::{
errors::{Result, Web5Error},
http,
};
use crate::errors::{Result, Web5Error};
use jsonschema::{Draft, JSONSchema};
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -30,7 +27,18 @@ pub(crate) fn validate_credential_schema(
}

let url = &credential_schema.id;
let json_schema = http::get_json::<serde_json::Value>(url)?;

let response = http_std::fetch(&url, None)?;

if !(200..300).contains(&response.status_code) {
return Err(Web5Error::JsonSchema(format!(
"failed to resolve status code {}",
response.status_code
)));
}

let json_schema = serde_json::from_slice::<serde_json::Value>(&response.body)?;

let compiled_schema = JSONSchema::options().compile(&json_schema).map_err(|err| {
Web5Error::JsonSchema(format!("unable to compile json schema {} {}", url, err))
})?;
Expand Down
Loading

0 comments on commit a3ba3ea

Please sign in to comment.