update request connecting and adding timeouts wow

This commit is contained in:
MeexReay 2025-01-25 17:09:09 +03:00
parent 38d4f30f55
commit 18f012835b
6 changed files with 153 additions and 150 deletions

6
Cargo.lock generated
View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "addr2line" name = "addr2line"
@ -448,9 +448,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.136" version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "336a0c23cf42a38d9eaa7cd22c7040d04e1228a19a933890805ffd00a16437d2" checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",

View File

@ -4,14 +4,14 @@ version = "0.2.1"
edition = "2021" edition = "2021"
repository = "https://git.meex.lol/MeexReay/ezhttp" 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" license-file = "LICENSE"
readme = "README.md" readme = "README.md"
keywords = ["http", "server", "site", "async"] keywords = ["http", "server", "site", "async"]
[dependencies] [dependencies]
urlencoding = "2.1.3" urlencoding = "2.1.3"
serde_json = "1.0.135" serde_json = "1.0.137"
tokio = { version = "1.43.0", features = ["full"] } tokio = { version = "1.43.0", features = ["full"] }
tokio-io-timeout = "1.2.0" tokio-io-timeout = "1.2.0"
threadpool = "1.8.1" threadpool = "1.8.1"

View File

@ -1,8 +1,6 @@
# EzHttp # EzHttp
Simple async http library with client and server 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 ## Setup
```toml ```toml

View File

@ -1,3 +1,5 @@
use std::time::Duration;
use crate::{error::HttpError, headers::Headers, prelude::HttpResponse, request::HttpRequest}; use crate::{error::HttpError, headers::Headers, prelude::HttpResponse, request::HttpRequest};
use super::{send_request, Proxy}; use super::{send_request, Proxy};
@ -5,15 +7,21 @@ use super::{send_request, Proxy};
/// Client that sends http requests /// Client that sends http requests
pub struct HttpClient { pub struct HttpClient {
proxy: Proxy, proxy: Proxy,
verify: bool, ssl_verify: bool,
headers: Headers headers: Headers,
connect_timeout: Option<Duration>,
write_timeout: Option<Duration>,
read_timeout: Option<Duration>
} }
/// [`HttpClient`](HttpClient) builder /// [`HttpClient`](HttpClient) builder
pub struct ClientBuilder { pub struct ClientBuilder {
proxy: Proxy, proxy: Proxy,
verify: bool, ssl_verify: bool,
headers: Headers headers: Headers,
connect_timeout: Option<Duration>,
write_timeout: Option<Duration>,
read_timeout: Option<Duration>
} }
impl ClientBuilder { impl ClientBuilder {
@ -21,8 +29,11 @@ impl ClientBuilder {
pub fn new() -> ClientBuilder { pub fn new() -> ClientBuilder {
ClientBuilder { ClientBuilder {
proxy: Proxy::None, proxy: Proxy::None,
verify: false, ssl_verify: false,
headers: Headers::new() headers: Headers::new(),
connect_timeout: None,
write_timeout: None,
read_timeout: None
} }
} }
@ -30,11 +41,40 @@ impl ClientBuilder {
pub fn build(self) -> HttpClient { pub fn build(self) -> HttpClient {
HttpClient { HttpClient {
proxy: self.proxy, proxy: self.proxy,
verify: self.verify, ssl_verify: self.ssl_verify,
headers: self.headers 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<Duration>, read: Option<Duration>, write: Option<Duration>) -> 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 /// Set client proxy
pub fn proxy(mut self, proxy: Proxy) -> Self { pub fn proxy(mut self, proxy: Proxy) -> Self {
self.proxy = proxy; self.proxy = proxy;
@ -42,8 +82,8 @@ impl ClientBuilder {
} }
/// Set is client have to verify ssl certificate /// Set is client have to verify ssl certificate
pub fn verify(mut self, verify: bool) -> Self { pub fn ssl_verify(mut self, verify: bool) -> Self {
self.verify = verify; self.ssl_verify = verify;
self self
} }
@ -68,7 +108,45 @@ impl HttpClient {
/// Sends a request and receives a response /// Sends a request and receives a response
pub async fn send(&self, request: HttpRequest) -> Result<HttpResponse, HttpError> { pub async fn send(&self, request: HttpRequest) -> Result<HttpResponse, HttpError> {
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<Duration> {
self.connect_timeout.clone()
}
/// Get read timeout
pub fn read_timeout(&self) -> Option<Duration> {
self.read_timeout.clone()
}
/// Get write timeout
pub fn write_timeout(&self) -> Option<Duration> {
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()
} }
} }

View File

@ -1,8 +1,9 @@
use std::pin::Pin; use std::{pin::Pin, time::Duration};
use base64::Engine; use base64::Engine;
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; 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_openssl::SslStream;
use tokio_socks::tcp::{Socks4Stream, Socks5Stream}; use tokio_socks::tcp::{Socks4Stream, Socks5Stream};
@ -18,139 +19,66 @@ pub use req_builder::*;
pub use client::*; pub use client::*;
pub use proxy::*; pub use proxy::*;
async fn send_request(request: HttpRequest, ssl_verify: bool, proxy: Proxy, headers: Headers) -> Result<HttpResponse, HttpError> { trait RequestStream: AsyncRead + AsyncWrite + Unpin + Send + Sync {}
let mut request = request.clone(); impl<T: AsyncRead + AsyncWrite + Unpin + Send + Sync> RequestStream for T {}
for (key, value) in headers.entries() { async fn connect_stream(proxy: Proxy, site_host: &str) -> Result<Box<dyn RequestStream>, HttpError> {
request.headers.put(key, value); 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)?),
})
}
request.headers.put("Connection", "close".to_string()); async fn send_request(
request.headers.put("Host", request.url.domain.to_string()); mut request: HttpRequest, ssl_verify: bool, proxy: Proxy, headers: Headers,
request.headers.put("Content-Length", request.body.as_bytes().len().to_string()); connect_timeout: Option<Duration>, write_timeout: Option<Duration>, read_timeout: Option<Duration>
) -> Result<HttpResponse, HttpError> {
for (key, value) in headers.entries() {
request.headers.put_default(key, value);
}
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); let site_host = format!("{}:{}", request.url.domain, request.url.port);
let stream: Box<dyn RequestStream> = 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?
}
};
match proxy { let mut stream = TimeoutStream::new(stream);
Proxy::Http { host, auth } => { stream.set_write_timeout(write_timeout);
let mut stream = TcpStream::connect(host).await.map_err(|_| HttpError::ConnectError)?; stream.set_read_timeout(read_timeout);
let mut stream = Box::pin(stream);
match auth { if request.url.scheme == "https" {
Some((user,password)) => stream.write_all(&[ let mut stream = ssl_wrapper(ssl_verify, request.url.domain.clone(), stream).await?;
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?; request.send(&mut stream).await?;
Ok(HttpResponse::recv(&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 { } 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?; request.send(&mut stream).await?;
Ok(HttpResponse::recv(&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)
}
}
} }
} }

View File

@ -16,7 +16,6 @@ pub mod prelude {
pub use super::server::*; pub use super::server::*;
pub use super::server::handler::*; pub use super::server::handler::*;
pub use super::server::starter::*; pub use super::server::starter::*;
pub use super::client::*;
pub use super::*; pub use super::*;
} }