From 64d945b10401662a64f19a53be85b671bd279706 Mon Sep 17 00:00:00 2001 From: MeexReay Date: Sat, 30 Nov 2024 00:28:50 +0300 Subject: [PATCH] proxy request and readme update --- Cargo.lock | 46 ++++++++++ Cargo.toml | 4 +- README.md | 33 ++++++- src/ezhttp/client/mod.rs | 171 +++++++++++++++++++++++++++++++------ src/ezhttp/client/proxy.rs | 10 ++- 5 files changed, 229 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c825a97..c168cb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.6.0" @@ -86,10 +92,17 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "ezhttp" version = "0.2.0" dependencies = [ + "base64", "lazy_static", "mime_guess", "openssl", @@ -100,6 +113,7 @@ dependencies = [ "tokio", "tokio-io-timeout", "tokio-openssl", + "tokio-socks", "urlencoding", ] @@ -585,6 +599,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "threadpool" version = "1.8.1" @@ -644,6 +678,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-socks" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +dependencies = [ + "either", + "futures-util", + "thiserror", + "tokio", +] + [[package]] name = "unicase" version = "2.8.0" diff --git a/Cargo.toml b/Cargo.toml index cb0ea86..aab8575 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,4 +20,6 @@ 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 +tokio-openssl = "0.6.5" +tokio-socks = "0.5.2" +base64 = "0.22.1" \ No newline at end of file diff --git a/README.md b/README.md index bab6cab..63bdae0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # EzHttp -Easy http server for small sites +Simple http library with client and server This library is under developement, so if you found any bugs, please write them to [Issues](https://github.com/MeexReay/ezhttp/issues) @@ -12,7 +12,27 @@ ezhttp = { git = "https://github.com/MeexReay/ezhttp" } # unstable ## Examples -Hello world example: +Client example: +```rust +use ezhttp::prelude::*; + +#[tokio::main] +async fn main() -> Result<(), HttpError> { + let client = HttpClient::builder().build(); // or HttpClient::default() + + let url = URL::from_str("https://google.com")?; + let request: HttpRequest = RequestBuilder::get(url).build(); + + let response: HttpResponse = client.send(request).await?; + + println!("response status: {}", response.status_code); + println!("response body: {} bytes", response.body.as_text().unwrap().len()); + + Ok(()) +} +``` + +Site example: ```rust use ezhttp::prelude::*; @@ -46,7 +66,14 @@ impl HttpServer for EzSite { #[tokio::main] async fn main() { - start_server(EzSite("Hello World!".to_string()), "localhost:8080").await.expect("http server error"); + HttpServerStarter::new( + EzSite("Hello World!".to_string()), + "localhost:8080" + ).timeout(Some(Duration::from_secs(5))) + .threads(5) + .start_forever() + .await + .expect("http server error"); } ``` diff --git a/src/ezhttp/client/mod.rs b/src/ezhttp/client/mod.rs index 0615b04..b8debec 100644 --- a/src/ezhttp/client/mod.rs +++ b/src/ezhttp/client/mod.rs @@ -1,11 +1,14 @@ use std::pin::Pin; +use base64::Engine; use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; -use tokio::net::TcpStream; +use tokio::{io::{AsyncReadExt, AsyncWriteExt}, net::TcpStream}; use tokio_openssl::SslStream; +use tokio_socks::tcp::{Socks4Stream, Socks5Stream}; use super::{error::HttpError, gen_multipart_boundary, headers::Headers, prelude::HttpResponse, request::HttpRequest}; +use base64::prelude::BASE64_STANDARD; pub mod req_builder; pub mod client; @@ -17,12 +20,8 @@ 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)?; +async fn send_request(request: HttpRequest, ssl_verify: bool, proxy: Proxy, headers: Headers) -> Result { + let mut request = request.clone(); for (key, value) in headers.entries() { request.headers.put(key, value); @@ -32,35 +31,151 @@ async fn send_request(request: HttpRequest, ssl_verify: bool, _proxy: Proxy, hea 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?; + let site_host = format!("{}:{}", request.url.domain, request.url.port); - Ok(HttpResponse::recv(&mut stream).await?) - } else if request.url.scheme == "https" { - let mut ssl_connector = SslConnector::builder(SslMethod::tls()) - .map_err(|_| HttpError::SslError)?; + 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?; - ssl_connector.set_verify(if ssl_verify { SslVerifyMode::PEER } else { SslVerifyMode::NONE }); + 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?; - let ssl_connector = ssl_connector.build(); + 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)?; - let ssl = ssl_connector - .configure() - .map_err(|_| HttpError::SslError)? - .into_ssl(&request.url.domain) - .map_err(|_| HttpError::SslError)?; + 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)?; - let mut wrapper = SslStream::new(ssl, stream) - .map_err(|_| HttpError::SslError)?; + HttpResponse::recv(&mut stream).await.map_err(|_| HttpError::ConnectError)?; - let mut wrapper = Pin::new(&mut wrapper); + 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?; - wrapper.as_mut().connect().await.map_err(|_| HttpError::SslError)?; + 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)?; - request.send(&mut wrapper).await?; + 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?; - Ok(HttpResponse::recv(&mut wrapper).await?) - } else { - Err(HttpError::UnknownScheme) + 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) + } + } } +} + +async fn ssl_wrapper(ssl_verify: bool, domain: String, stream: S) -> Result>>, HttpError> { + 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(&domain) + .map_err(|_| HttpError::SslError)?; + + let wrapper = SslStream::new(ssl, stream) + .map_err(|_| HttpError::SslError)?; + + let mut wrapper = Box::pin(wrapper); + + wrapper.as_mut().connect().await.map_err(|_| HttpError::SslError)?; + + Ok(wrapper) } \ No newline at end of file diff --git a/src/ezhttp/client/proxy.rs b/src/ezhttp/client/proxy.rs index 25335d5..a229526 100644 --- a/src/ezhttp/client/proxy.rs +++ b/src/ezhttp/client/proxy.rs @@ -4,7 +4,7 @@ use std::net::{ToSocketAddrs, SocketAddr}; pub enum Proxy { None, Socks5 { host: SocketAddr, auth: Option<(String, String)> }, - Socks4 { host: SocketAddr, user: String }, + Socks4 { host: SocketAddr, user: Option }, Http { host: SocketAddr, auth: Option<(String, String)> }, Https { host: SocketAddr, auth: Option<(String, String)> }, } @@ -22,8 +22,12 @@ impl Proxy { 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 socks4(host: impl ToSocketAddrs) -> Self { + Self::Socks4 { host: host.to_socket_addrs().unwrap().next().unwrap(), user: None } + } + + pub fn socks4_with_auth(host: impl ToSocketAddrs, user_id: String) -> Self { + Self::Socks4 { host: host.to_socket_addrs().unwrap().next().unwrap(), user: Some(user_id) } } pub fn http(host: impl ToSocketAddrs) -> Self {