diff --git a/Cargo.lock b/Cargo.lock index 7170829..12c0a42 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -448,9 +448,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.136" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336a0c23cf42a38d9eaa7cd22c7040d04e1228a19a933890805ffd00a16437d2" +checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" dependencies = [ "itoa", "memchr", diff --git a/Cargo.toml b/Cargo.toml index dad139b..3b675fc 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,14 +4,14 @@ version = "0.2.1" edition = "2021" repository = "https://git.meex.lol/MeexReay/ezhttp" -description = "easy http server for small sites" +description = "Simple async http library with client and server" license-file = "LICENSE" readme = "README.md" keywords = ["http", "server", "site", "async"] [dependencies] urlencoding = "2.1.3" -serde_json = "1.0.135" +serde_json = "1.0.137" tokio = { version = "1.43.0", features = ["full"] } tokio-io-timeout = "1.2.0" threadpool = "1.8.1" diff --git a/README.md b/README.md index 3df5db3..b5f70f5 100755 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # EzHttp Simple async http library with client and server -This library is under developement, so if you found any bugs, please write them to [Issues](https://git.meex.lol/MeexReay/ezhttp/issues) - ## Setup ```toml diff --git a/src/client/client.rs b/src/client/client.rs index 24da97b..28b7883 100755 --- a/src/client/client.rs +++ b/src/client/client.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use crate::{error::HttpError, headers::Headers, prelude::HttpResponse, request::HttpRequest}; use super::{send_request, Proxy}; @@ -5,15 +7,21 @@ use super::{send_request, Proxy}; /// Client that sends http requests pub struct HttpClient { proxy: Proxy, - verify: bool, - headers: Headers + ssl_verify: bool, + headers: Headers, + connect_timeout: Option, + write_timeout: Option, + read_timeout: Option } /// [`HttpClient`](HttpClient) builder pub struct ClientBuilder { proxy: Proxy, - verify: bool, - headers: Headers + ssl_verify: bool, + headers: Headers, + connect_timeout: Option, + write_timeout: Option, + read_timeout: Option } impl ClientBuilder { @@ -21,8 +29,11 @@ impl ClientBuilder { pub fn new() -> ClientBuilder { ClientBuilder { proxy: Proxy::None, - verify: false, - headers: Headers::new() + ssl_verify: false, + headers: Headers::new(), + connect_timeout: None, + write_timeout: None, + read_timeout: None } } @@ -30,11 +41,40 @@ impl ClientBuilder { pub fn build(self) -> HttpClient { HttpClient { proxy: self.proxy, - verify: self.verify, - headers: self.headers + ssl_verify: self.ssl_verify, + headers: self.headers, + connect_timeout: self.connect_timeout, + write_timeout: self.write_timeout, + read_timeout: self.read_timeout } } + /// Set request timeouts0 + pub fn timeout(mut self, connect: Option, read: Option, write: Option) -> Self { + self.connect_timeout = connect; + self.read_timeout = read; + self.write_timeout = write; + self + } + + /// Set connect timeout + pub fn connect_timeout(mut self, timeout: Duration) -> Self { + self.connect_timeout = Some(timeout); + self + } + + /// Set read timeout + pub fn read_timeout(mut self, timeout: Duration) -> Self { + self.read_timeout = Some(timeout); + self + } + + /// Set write timeout + pub fn write_timeout(mut self, timeout: Duration) -> Self { + self.write_timeout = Some(timeout); + self + } + /// Set client proxy pub fn proxy(mut self, proxy: Proxy) -> Self { self.proxy = proxy; @@ -42,8 +82,8 @@ impl ClientBuilder { } /// Set is client have to verify ssl certificate - pub fn verify(mut self, verify: bool) -> Self { - self.verify = verify; + pub fn ssl_verify(mut self, verify: bool) -> Self { + self.ssl_verify = verify; self } @@ -68,7 +108,45 @@ impl HttpClient { /// Sends a request and receives a response pub async fn send(&self, request: HttpRequest) -> Result { - send_request(request, self.verify, self.proxy.clone(), self.headers.clone()).await + send_request( + request, + self.ssl_verify, + self.proxy.clone(), + self.headers.clone(), + self.connect_timeout, + self.write_timeout, + self.read_timeout + ).await + } + + /// Get connect timeout + pub fn connect_timeout(&self) -> Option { + self.connect_timeout.clone() + } + + /// Get read timeout + pub fn read_timeout(&self) -> Option { + self.read_timeout.clone() + } + + /// Get write timeout + pub fn write_timeout(&self) -> Option { + self.write_timeout.clone() + } + + /// Get client proxy + pub fn proxy(&self) -> Proxy { + self.proxy.clone() + } + + /// Get is client have to verify ssl certificate + pub fn ssl_verify(&self) -> bool { + self.ssl_verify + } + + /// Get default headers + pub fn headers(&self) -> Headers { + self.headers.clone() } } diff --git a/src/client/mod.rs b/src/client/mod.rs index 651567c..a5b4ca4 100755 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,8 +1,9 @@ -use std::pin::Pin; +use std::{pin::Pin, time::Duration}; use base64::Engine; use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; -use tokio::{io::{AsyncReadExt, AsyncWriteExt}, net::TcpStream}; +use tokio::{io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, net::TcpStream}; +use tokio_io_timeout::TimeoutStream; use tokio_openssl::SslStream; use tokio_socks::tcp::{Socks4Stream, Socks5Stream}; @@ -18,139 +19,66 @@ pub use req_builder::*; pub use client::*; pub use proxy::*; -async fn send_request(request: HttpRequest, ssl_verify: bool, proxy: Proxy, headers: Headers) -> Result { - let mut request = request.clone(); +trait RequestStream: AsyncRead + AsyncWrite + Unpin + Send + Sync {} +impl RequestStream for T {} +async fn connect_stream(proxy: Proxy, site_host: &str) -> Result, HttpError> { + Ok(match proxy { + Proxy::Http { host, auth } | Proxy::Https { host, auth } => { + let mut stream = TcpStream::connect(host).await.map_err(|_| HttpError::ConnectError)?; + let auth_header = auth.map(|(u, p)| format!("Proxy-Authorization: basic {}\r\n", BASE64_STANDARD.encode(format!("{u}:{p}")))); + let connect_request = format!("CONNECT {site_host} HTTP/1.1\r\nHost: {site_host}\r\n{}\r\n", auth_header.unwrap_or_default()); + stream.write_all(connect_request.as_bytes()).await.map_err(|_| HttpError::ConnectError)?; + HttpResponse::recv(&mut stream).await.map_err(|_| HttpError::ConnectError)?; + Box::new(stream) + } + Proxy::Socks4 { host, user } => Box::new(match user { + Some(user) => Socks4Stream::connect_with_userid(host, site_host, &user).await.map_err(|_| HttpError::ConnectError)?, + None => Socks4Stream::connect(host, site_host).await.map_err(|_| HttpError::ConnectError)?, + }), + Proxy::Socks5 { host, auth } => Box::new(match auth { + Some((u, p)) => Socks5Stream::connect_with_password(host, site_host, &u, &p).await.map_err(|_| HttpError::ConnectError)?, + None => Socks5Stream::connect(host, site_host).await.map_err(|_| HttpError::ConnectError)?, + }), + Proxy::None => Box::new(TcpStream::connect(site_host).await.map_err(|_| HttpError::ConnectError)?), + }) +} + +async fn send_request( + mut request: HttpRequest, ssl_verify: bool, proxy: Proxy, headers: Headers, + connect_timeout: Option, write_timeout: Option, read_timeout: Option +) -> Result { for (key, value) in headers.entries() { - request.headers.put(key, value); + request.headers.put_default(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()); - + request.headers.put_default("Connection", "close".to_string()); + request.headers.put_default("Host", request.url.domain.to_string()); + request.headers.put_default("Content-Length", request.body.as_bytes().len().to_string()); + let site_host = format!("{}:{}", request.url.domain, request.url.port); - - match proxy { - Proxy::Http { host, auth } => { - let mut stream = TcpStream::connect(host).await.map_err(|_| HttpError::ConnectError)?; - - match auth { - Some((user,password)) => stream.write_all(&[ - b"CONNECT ", site_host.as_bytes(), b" HTTP/1.1\r\n", - b"Host: ", site_host.as_bytes(), b"\r\n", - b"Proxy-Authorization: basic ", BASE64_STANDARD.encode(format!("{user}:{password}")).as_bytes(), b"\r\n\r\n", - ].concat()).await, - None => stream.write_all(&[ - b"CONNECT ", site_host.as_bytes(), b" HTTP/1.1\r\n", - b"Host: ", site_host.as_bytes(), b"\r\n\r\n", - ].concat()).await - }.map_err(|_| HttpError::ConnectError)?; - - HttpResponse::recv(&mut stream).await.map_err(|_| HttpError::ConnectError)?; - - if request.url.scheme == "http" { - request.send(&mut stream).await?; - - Ok(HttpResponse::recv(&mut stream).await?) - } else if request.url.scheme == "https" { - let mut wrapper = ssl_wrapper(ssl_verify, request.url.domain.clone(), stream).await?; - - request.send(&mut wrapper).await?; - - Ok(HttpResponse::recv(&mut wrapper).await?) - } else { - Err(HttpError::UnknownScheme) - } - } - Proxy::Https { host, auth } => { - let mut stream = TcpStream::connect(host).await.map_err(|_| HttpError::ConnectError)?; - - match auth { - Some((user,password)) => stream.write_all(&[ - b"CONNECT ", site_host.as_bytes(), b" HTTP/1.1\r\n", - b"Host: ", site_host.as_bytes(), b"\r\n", - b"Proxy-Authorization: basic ", BASE64_STANDARD.encode(format!("{user}:{password}")).as_bytes(), b"\r\n\r\n", - ].concat()).await, - None => stream.write_all(&[ - b"CONNECT ", site_host.as_bytes(), b" HTTP/1.1\r\n", - b"Host: ", site_host.as_bytes(), b"\r\n\r\n", - ].concat()).await - }.map_err(|_| HttpError::ConnectError)?; - - HttpResponse::recv(&mut stream).await.map_err(|_| HttpError::ConnectError)?; - - if request.url.scheme == "http" { - request.send(&mut stream).await?; - - Ok(HttpResponse::recv(&mut stream).await?) - } else if request.url.scheme == "https" { - let mut wrapper = ssl_wrapper(ssl_verify, request.url.domain.clone(), stream).await?; - - request.send(&mut wrapper).await?; - - Ok(HttpResponse::recv(&mut wrapper).await?) - } else { - Err(HttpError::UnknownScheme) - } - } - Proxy::Socks4 { host, user } => { - let mut stream = match user { - Some(user) => Socks4Stream::connect_with_userid(host, site_host, &user).await, - None => Socks4Stream::connect(host, site_host).await - }.map_err(|_| HttpError::ConnectError)?; - - if request.url.scheme == "http" { - request.send(&mut stream).await?; - - Ok(HttpResponse::recv(&mut stream).await?) - } else if request.url.scheme == "https" { - let mut wrapper = ssl_wrapper(ssl_verify, request.url.domain.clone(), stream).await?; - - request.send(&mut wrapper).await?; - - Ok(HttpResponse::recv(&mut wrapper).await?) - } else { - Err(HttpError::UnknownScheme) - } - } - Proxy::Socks5 { host, auth } => { - let mut stream = match auth { - Some(auth) => Socks5Stream::connect_with_password(host, site_host, &auth.0, &auth.1).await, - None => Socks5Stream::connect(host, site_host).await - }.map_err(|_| HttpError::ConnectError)?; - - if request.url.scheme == "http" { - request.send(&mut stream).await?; - - Ok(HttpResponse::recv(&mut stream).await?) - } else if request.url.scheme == "https" { - let mut wrapper = ssl_wrapper(ssl_verify, request.url.domain.clone(), stream).await?; - - request.send(&mut wrapper).await?; - - Ok(HttpResponse::recv(&mut wrapper).await?) - } else { - Err(HttpError::UnknownScheme) - } - } - Proxy::None => { - let mut stream = TcpStream::connect(site_host).await.map_err(|_| HttpError::ConnectError)?; - - if request.url.scheme == "http" { - request.send(&mut stream).await?; - - Ok(HttpResponse::recv(&mut stream).await?) - } else if request.url.scheme == "https" { - let mut wrapper = ssl_wrapper(ssl_verify, request.url.domain.clone(), stream).await?; - - request.send(&mut wrapper).await?; - - Ok(HttpResponse::recv(&mut wrapper).await?) - } else { - Err(HttpError::UnknownScheme) - } + let stream: Box = match connect_timeout { + Some(connect_timeout) => { + tokio::time::timeout( + connect_timeout, + connect_stream(proxy, &site_host) + ).await.map_err(|_| HttpError::ConnectError)?? + }, None => { + connect_stream(proxy, &site_host).await? } + }; + + let mut stream = TimeoutStream::new(stream); + stream.set_write_timeout(write_timeout); + stream.set_read_timeout(read_timeout); + let mut stream = Box::pin(stream); + + if request.url.scheme == "https" { + let mut stream = ssl_wrapper(ssl_verify, request.url.domain.clone(), stream).await?; + request.send(&mut stream).await?; + Ok(HttpResponse::recv(&mut stream).await?) + } else { + request.send(&mut stream).await?; + Ok(HttpResponse::recv(&mut stream).await?) } } @@ -176,4 +104,4 @@ async fn ssl_wrapper(ssl_verify: bool, domain: wrapper.as_mut().connect().await.map_err(|_| HttpError::SslError)?; Ok(wrapper) -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index 4a968d0..3cfb48e 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,6 @@ pub mod prelude { pub use super::server::*; pub use super::server::handler::*; pub use super::server::starter::*; - pub use super::client::*; pub use super::*; }