diff --git a/Cargo.lock b/Cargo.lock index 275b98a..98f39a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,15 +92,32 @@ version = "0.1.6" dependencies = [ "lazy_static", "mime_guess", + "openssl", "rand", "rusty_pool", "serde_json", "threadpool", "tokio", "tokio-io-timeout", + "tokio-openssl", "urlencoding", ] +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "futures" version = "0.3.30" @@ -303,6 +320,50 @@ dependencies = [ "memchr", ] +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -338,6 +399,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -566,6 +633,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-openssl" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59df6849caa43bb7567f9a36f863c447d95a11d5903c9cc334ba32576a27eadd" +dependencies = [ + "openssl", + "openssl-sys", + "tokio", +] + [[package]] name = "unicase" version = "2.8.0" @@ -584,6 +662,12 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 8bf3815..cd3f4ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,5 @@ threadpool = "1.8.1" lazy_static = "1.5.0" rand = "0.8.5" mime_guess = "2.0.5" +openssl = "0.10.68" +tokio-openssl = "0.6.5" \ No newline at end of file diff --git a/examples/request_meex.rs b/examples/request_meex.rs new file mode 100644 index 0000000..8f04b6b --- /dev/null +++ b/examples/request_meex.rs @@ -0,0 +1,16 @@ +use std::str::FromStr; + +use ezhttp::{client::{HttpClient, RequestBuilder}, request::URL}; + +#[tokio::main] +async fn main() { + let response = HttpClient::default().send( + RequestBuilder::get( + URL::from_str("https://meex.lol/dku?key=value#hex_id") + .expect("url error") + ).build() + ).await.expect("request error"); + println!("status code: {}", response.status_code); + println!("headers: {}", response.headers.entries().iter().map(|o| format!("{}: {}", o.0, o.1)).collect::>().join("; ")); + println!("body: {} bytes", response.body.as_text().unwrap().len()); +} diff --git a/src/ezhttp/body.rs b/src/ezhttp/body.rs index 7892758..646385b 100644 --- a/src/ezhttp/body.rs +++ b/src/ezhttp/body.rs @@ -160,6 +160,7 @@ impl Default for Body { } } +#[derive(Clone,Debug)] pub struct Part { pub name: String, pub body: Body, diff --git a/src/ezhttp/client/client.rs b/src/ezhttp/client/client.rs new file mode 100644 index 0000000..f101040 --- /dev/null +++ b/src/ezhttp/client/client.rs @@ -0,0 +1,71 @@ +use crate::{error::HttpError, headers::Headers, prelude::HttpResponse, request::HttpRequest}; + +use super::{send_request, Proxy}; + +pub struct HttpClient { + proxy: Proxy, + verify: bool, + headers: Headers +} + +pub struct ClientBuilder { + proxy: Proxy, + verify: bool, + headers: Headers +} + +impl ClientBuilder { + pub fn new() -> ClientBuilder { + ClientBuilder { + proxy: Proxy::None, + verify: false, + headers: Headers::new() + } + } + + pub fn build(self) -> HttpClient { + HttpClient { + proxy: self.proxy, + verify: self.verify, + headers: self.headers + } + } + + pub fn proxy(mut self, proxy: Proxy) -> Self { + self.proxy = proxy; + self + } + + pub fn verify(mut self, verify: bool) -> Self { + self.verify = verify; + self + } + + + pub fn headers(mut self, headers: Headers) -> Self { + self.headers = headers; + self + } + + pub fn header(mut self, name: impl ToString, value: impl ToString) -> Self { + self.headers.put(name, value.to_string()); + self + } +} + +impl HttpClient { + pub fn builder() -> ClientBuilder { + ClientBuilder::new() + } + + pub async fn send(&self, request: HttpRequest) -> Result { + send_request(request, self.verify, self.proxy.clone(), self.headers.clone()).await + } +} + +impl Default for HttpClient { + fn default() -> Self { + ClientBuilder::new().build() + } +} + diff --git a/src/ezhttp/client/mod.rs b/src/ezhttp/client/mod.rs index e69de29..7e4404d 100644 --- a/src/ezhttp/client/mod.rs +++ b/src/ezhttp/client/mod.rs @@ -0,0 +1,66 @@ +use std::pin::Pin; + +use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; +use tokio::net::TcpStream; +use tokio_openssl::SslStream; + +use super::{error::HttpError, gen_multipart_boundary, headers::Headers, prelude::HttpResponse, request::HttpRequest}; + + +pub mod req_builder; +pub mod client; +pub mod proxy; + +pub use req_builder::*; +pub use client::*; +pub use proxy::*; + + +// TODO: proxy support +async fn send_request(request: HttpRequest, ssl_verify: bool, _proxy: Proxy, headers: Headers) -> Result { + let mut request = request; + + let mut stream = TcpStream::connect( + format!("{}:{}", request.url.domain, request.url.port) + ).await.map_err(|_| HttpError::ConnectError)?; + + for (key, value) in headers.entries() { + request.headers.put(key, value); + } + + request.headers.put("Connection", "close".to_string()); + request.headers.put("Host", request.url.domain.to_string()); + request.headers.put("Content-Length", request.body.as_bytes().len().to_string()); + + if request.url.scheme == "http" { + request.send(&mut stream).await?; + + Ok(HttpResponse::recv(&mut stream).await?) + } else if request.url.scheme == "https" { + let mut ssl_connector = SslConnector::builder(SslMethod::tls()) + .map_err(|_| HttpError::SslError)?; + + ssl_connector.set_verify(if ssl_verify { SslVerifyMode::PEER } else { SslVerifyMode::NONE }); + + let ssl_connector = ssl_connector.build(); + + let ssl = ssl_connector + .configure() + .map_err(|_| HttpError::SslError)? + .into_ssl(&request.url.domain) + .map_err(|_| HttpError::SslError)?; + + let mut wrapper = SslStream::new(ssl, stream) + .map_err(|_| HttpError::SslError)?; + + let mut wrapper = Pin::new(&mut wrapper); + + wrapper.as_mut().connect().await.map_err(|_| HttpError::SslError)?; + + request.send(&mut wrapper).await?; + + Ok(HttpResponse::recv(&mut wrapper).await?) + } else { + Err(HttpError::UnknownScheme) + } +} \ No newline at end of file diff --git a/src/ezhttp/client/proxy.rs b/src/ezhttp/client/proxy.rs new file mode 100644 index 0000000..25335d5 --- /dev/null +++ b/src/ezhttp/client/proxy.rs @@ -0,0 +1,44 @@ +use std::net::{ToSocketAddrs, SocketAddr}; + +#[derive(Clone, Debug)] +pub enum Proxy { + None, + Socks5 { host: SocketAddr, auth: Option<(String, String)> }, + Socks4 { host: SocketAddr, user: String }, + Http { host: SocketAddr, auth: Option<(String, String)> }, + Https { host: SocketAddr, auth: Option<(String, String)> }, +} + +impl Proxy { + pub fn none() -> Self { + Self::None + } + + pub fn socks5(host: impl ToSocketAddrs) -> Self { + Self::Socks5 { host: host.to_socket_addrs().unwrap().next().unwrap(), auth: None } + } + + pub fn socks5_with_auth(host: impl ToSocketAddrs, user: String, password: String) -> Self { + Self::Socks5 { host: host.to_socket_addrs().unwrap().next().unwrap(), auth: Some((user, password)) } + } + + pub fn socks4(host: impl ToSocketAddrs, user_id: String) -> Self { + Self::Socks4 { host: host.to_socket_addrs().unwrap().next().unwrap(), user: user_id } + } + + pub fn http(host: impl ToSocketAddrs) -> Self { + Self::Http { host: host.to_socket_addrs().unwrap().next().unwrap(), auth: None } + } + + pub fn http_with_auth(host: impl ToSocketAddrs, user: String, password: String) -> Self { + Self::Http { host: host.to_socket_addrs().unwrap().next().unwrap(), auth: Some((user, password)) } + } + + pub fn https(host: impl ToSocketAddrs) -> Self { + Self::Https { host: host.to_socket_addrs().unwrap().next().unwrap(), auth: None } + } + + pub fn https_with_auth(host: impl ToSocketAddrs, user: String, password: String) -> Self { + Self::Https { host: host.to_socket_addrs().unwrap().next().unwrap(), auth: Some((user, password)) } + } +} \ No newline at end of file diff --git a/src/ezhttp/client/req_builder.rs b/src/ezhttp/client/req_builder.rs new file mode 100644 index 0000000..7f24e98 --- /dev/null +++ b/src/ezhttp/client/req_builder.rs @@ -0,0 +1,100 @@ +use std::{collections::HashMap, net::ToSocketAddrs}; + +use serde_json::Value; + +use super::{super::body::{Body, Part}, gen_multipart_boundary, super::headers::Headers, super::request::{HttpRequest, URL}}; + +pub struct RequestBuilder { + method: String, + url: URL, + headers: Headers, + body: Option +} + +impl RequestBuilder { + pub fn new(method: String, url: URL) -> Self { + RequestBuilder { + method, + url, + headers: Headers::new(), + body: None + } + } + + pub fn get(url: URL) -> Self { Self::new("GET".to_string(), url) } + pub fn head(url: URL) -> Self { Self::new("HEAD".to_string(), url) } + pub fn post(url: URL) -> Self { Self::new("POST".to_string(), url) } + pub fn put(url: URL) -> Self { Self::new("PUT".to_string(), url) } + pub fn delete(url: URL) -> Self { Self::new("DELETE".to_string(), url) } + pub fn connect(url: URL) -> Self { Self::new("CONNECT".to_string(), url) } + pub fn options(url: URL) -> Self { Self::new("OPTIONS".to_string(), url) } + pub fn trace(url: URL) -> Self { Self::new("TRACE".to_string(), url) } + pub fn patch(url: URL) -> Self { Self::new("PATCH".to_string(), url) } + + pub fn url(mut self, url: URL) -> Self { + self.url = url; + self + } + + pub fn method(mut self, method: String) -> Self { + self.method = method; + self + } + + pub fn headers(mut self, headers: Headers) -> Self { + self.headers = headers; + self + } + + pub fn header(mut self, name: impl ToString, value: impl ToString) -> Self { + self.headers.put(name, value.to_string()); + self + } + + pub fn body(mut self, body: Body) -> Self { + self.body = Some(body); + self + } + + pub fn text(mut self, text: impl ToString) -> Self { + self.body = Some(Body::from_text(text.to_string().as_str())); + self + } + + pub fn json(mut self, json: Value) -> Self { + self.body = Some(Body::from_json(json)); + self + } + + pub fn bytes(mut self, bytes: &[u8]) -> Self { + self.body = Some(Body::from_bytes(bytes)); + self + } + + pub fn multipart(mut self, parts: &[Part]) -> Self { + let boundary = gen_multipart_boundary(); + self.headers.put("Content-Type", format!("multipart/form-data; boundary={}", boundary.clone())); + self.body = Some(Body::from_multipart(parts.to_vec(), boundary)); + self + } + + pub fn url_query(mut self, query: &[(impl ToString, impl ToString)]) -> Self { + self.url.query = HashMap::from_iter(query.iter().map(|o| (o.0.to_string(), o.1.to_string()))); + self + } + + pub fn body_query(mut self, query: &[(impl ToString, impl ToString)]) -> Self { + self.body = Some(Body::from_query(HashMap::from_iter(query.iter().map(|o| (o.0.to_string(), o.1.to_string()))))); + self + } + + pub fn build(self) -> HttpRequest { + HttpRequest { + url: self.url, + method: self.method, + addr: "localhost:80".to_socket_addrs().unwrap().next().unwrap(), + headers: self.headers, + body: self.body.unwrap_or(Body::default()) + } + } +} \ No newline at end of file diff --git a/src/ezhttp/error.rs b/src/ezhttp/error.rs index 98cd2d0..03db81c 100644 --- a/src/ezhttp/error.rs +++ b/src/ezhttp/error.rs @@ -14,7 +14,11 @@ pub enum HttpError { WriteBodyError, InvalidStatus, RequestError, - UrlError + UrlError, + ConnectError, + ShutdownError, + SslError, + UnknownScheme } impl std::fmt::Display for HttpError { diff --git a/src/ezhttp/mod.rs b/src/ezhttp/mod.rs index 70e9398..fbe39c3 100644 --- a/src/ezhttp/mod.rs +++ b/src/ezhttp/mod.rs @@ -7,7 +7,6 @@ pub mod server; pub mod client; pub mod prelude { - pub use super::*; pub use super::error::*; pub use super::headers::*; pub use super::request::*; diff --git a/src/ezhttp/request.rs b/src/ezhttp/request.rs index ce85ba5..597fa9b 100644 --- a/src/ezhttp/request.rs +++ b/src/ezhttp/request.rs @@ -1,4 +1,4 @@ -use super::{body::{Body, Part}, gen_multipart_boundary, read_line_crlf, headers::Headers, HttpError}; +use super::{body::{Body, Part}, client::RequestBuilder, gen_multipart_boundary, headers::Headers, read_line_crlf, HttpError}; use std::{ collections::HashMap, fmt::{Debug, Display}, net::SocketAddr, str::FromStr @@ -184,7 +184,7 @@ impl HttpRequest { self.headers.send(stream).await?; - stream.write_all(b"\r\n").await.map_err(|_| HttpError::WriteHeadError)?; + stream.write_all(b"\r\n").await.map_err(|_| HttpError::WriteBodyError)?; self.body.send(stream).await?; @@ -206,6 +206,10 @@ impl HttpRequest { self.body = Body::from_multipart(parts, boundary); Some(()) } + + pub fn builder(method: String, url: URL) -> RequestBuilder { + RequestBuilder::new(method, url) + } } diff --git a/src/ezhttp/response.rs b/src/ezhttp/response.rs index 98ac138..3ba1d99 100644 --- a/src/ezhttp/response.rs +++ b/src/ezhttp/response.rs @@ -57,7 +57,7 @@ impl HttpResponse { self.headers.send(stream).await?; - stream.write_all(b"\r\n").await.map_err(|_| HttpError::WriteHeadError)?; + stream.write_all(b"\r\n").await.map_err(|_| HttpError::WriteBodyError)?; self.body.send(stream).await?;