http client

This commit is contained in:
MeexReay 2024-11-27 23:28:25 +03:00
parent 24db811c57
commit d59523b009
12 changed files with 396 additions and 5 deletions

84
Cargo.lock generated
View File

@ -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"

View File

@ -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"

16
examples/request_meex.rs Normal file
View File

@ -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::<Vec<String>>().join("; "));
println!("body: {} bytes", response.body.as_text().unwrap().len());
}

View File

@ -160,6 +160,7 @@ impl Default for Body {
}
}
#[derive(Clone,Debug)]
pub struct Part {
pub name: String,
pub body: Body,

View File

@ -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<HttpResponse, HttpError> {
send_request(request, self.verify, self.proxy.clone(), self.headers.clone()).await
}
}
impl Default for HttpClient {
fn default() -> Self {
ClientBuilder::new().build()
}
}

View File

@ -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<HttpResponse, HttpError> {
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)
}
}

View File

@ -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)) }
}
}

View File

@ -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<Body>
}
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())
}
}
}

View File

@ -14,7 +14,11 @@ pub enum HttpError {
WriteBodyError,
InvalidStatus,
RequestError,
UrlError
UrlError,
ConnectError,
ShutdownError,
SslError,
UnknownScheme
}
impl std::fmt::Display for HttpError {

View File

@ -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::*;

View File

@ -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)
}
}

View File

@ -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?;