diff --git a/examples/request_meex.rs b/examples/request_meex.rs index 80c6191..ebf8824 100755 --- a/examples/request_meex.rs +++ b/examples/request_meex.rs @@ -1,9 +1,11 @@ -use std::{error::Error, str::FromStr, time::Duration}; +use std::{error::Error, time::Duration}; -use ezhttp::{client::{ClientBuilder, RequestBuilder}, request::URL}; +use ezhttp::{client::{ClientBuilder, RequestBuilder}, request::IntoURL}; #[tokio::main] async fn main() -> Result<(), Box> { + dbg!("https://meex.lol/dku?key=value#hex_id".to_url().unwrap().to_string()); + let client = ClientBuilder::new() .ssl_verify(false) .connect_timeout(Duration::from_secs(5)) @@ -12,7 +14,7 @@ async fn main() -> Result<(), Box> { .header("User-Agent", "EzHttp/0.1.0") .build(); - let request = RequestBuilder::get(URL::from_str("https://meex.lol/dku?key=value#hex_id")?).build(); + let request = RequestBuilder::get("https://meex.lol/dku?key=value#hex_id"); println!("request: {:?}", &request); diff --git a/examples/simple_site.rs b/examples/simple_site.rs index da052f5..88a47ad 100755 --- a/examples/simple_site.rs +++ b/examples/simple_site.rs @@ -66,7 +66,7 @@ impl EzSite { #[async_trait] impl HttpServer for EzSite { async fn on_request(&self, req: &HttpRequest) -> Option> { - println!("{} > {} {}", req.addr, req.method, req.url.to_path_string()); + println!("{} > {} {}", req.addr?, req.method, req.url.to_string()); if let Some(resp) = self.get_main_page(req).await { Some(resp.as_box()) diff --git a/examples/small_site.rs b/examples/small_site.rs index cb53108..0503181 100755 --- a/examples/small_site.rs +++ b/examples/small_site.rs @@ -6,7 +6,7 @@ struct EzSite(String); #[async_trait] impl HttpServer for EzSite { async fn on_request(&self, req: &HttpRequest) -> Option> { - println!("{} > {} {}", req.addr, req.method, req.url.to_path_string()); + println!("{} > {} {}", req.addr?, req.method, req.url.to_string()); if req.url.path == "/" { Some(HttpResponse::new( diff --git a/src/client/client.rs b/src/client/client.rs index ec0200d..f705685 100755 --- a/src/client/client.rs +++ b/src/client/client.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use crate::{error::HttpError, headers::Headers, prelude::HttpResponse, request::HttpRequest}; +use crate::{error::HttpError, headers::Headers, prelude::HttpResponse, request::IntoRequest}; use super::{send_request, Proxy}; @@ -107,9 +107,9 @@ impl HttpClient { } /// Sends a request and receives a response - pub async fn send(&self, request: HttpRequest) -> Result { + pub async fn send(&self, request: impl IntoRequest) -> Result { send_request( - request, + request.to_request()?, self.ssl_verify, self.proxy.clone(), self.headers.clone(), diff --git a/src/client/mod.rs b/src/client/mod.rs index a5b4ca4..b222ff6 100755 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -51,11 +51,14 @@ async fn send_request( for (key, value) in headers.entries() { request.headers.put_default(key, value); } + + let root = request.clone().url.root.ok_or(HttpError::UrlNeedsRootError)?; + request.headers.put_default("Connection", "close".to_string()); - request.headers.put_default("Host", request.url.domain.to_string()); + request.headers.put_default("Host", root.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!("{}:{}", root.domain, root.port); let stream: Box = match connect_timeout { Some(connect_timeout) => { tokio::time::timeout( @@ -72,8 +75,8 @@ async fn send_request( 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?; + if root.scheme == "https" { + let mut stream = ssl_wrapper(ssl_verify, root.domain.clone(), stream).await?; request.send(&mut stream).await?; Ok(HttpResponse::recv(&mut stream).await?) } else { diff --git a/src/client/req_builder.rs b/src/client/req_builder.rs index 19fa9ad..4ed1837 100755 --- a/src/client/req_builder.rs +++ b/src/client/req_builder.rs @@ -1,58 +1,63 @@ -use std::{collections::HashMap, net::ToSocketAddrs}; +use std::collections::HashMap; use serde_json::Value; -use super::{super::body::{Body, Part}, gen_multipart_boundary, super::headers::Headers, super::request::{HttpRequest, URL}}; +use crate::{error::HttpError, request::{IntoRequest, IntoURL}}; + +use super::{super::body::{Body, Part}, gen_multipart_boundary, super::headers::Headers, super::request::HttpRequest}; /// Builder for [`HttpRequest`](HttpRequest) +#[derive(Debug, Clone)] pub struct RequestBuilder { method: String, - url: URL, + url: String, headers: Headers, - body: Option + body: Option, + url_query: Option> } impl RequestBuilder { /// Create builder with a custom method - pub fn new(method: String, url: URL) -> Self { + pub fn new(method: String, url: impl IntoURL) -> Self { RequestBuilder { method, - url, + url: url.to_string(), headers: Headers::new(), - body: None + body: None, + url_query: None } } /// Create builder for a GET request - pub fn get(url: URL) -> Self { Self::new("GET".to_string(), url) } + pub fn get(url: impl IntoURL) -> Self { Self::new("GET".to_string(), url) } /// Create builder for a HEAD request - pub fn head(url: URL) -> Self { Self::new("HEAD".to_string(), url) } + pub fn head(url: impl IntoURL) -> Self { Self::new("HEAD".to_string(), url) } /// Create builder for a POST request - pub fn post(url: URL) -> Self { Self::new("POST".to_string(), url) } + pub fn post(url: impl IntoURL) -> Self { Self::new("POST".to_string(), url) } /// Create builder for a PUT request - pub fn put(url: URL) -> Self { Self::new("PUT".to_string(), url) } + pub fn put(url: impl IntoURL) -> Self { Self::new("PUT".to_string(), url) } /// Create builder for a DELETE request - pub fn delete(url: URL) -> Self { Self::new("DELETE".to_string(), url) } + pub fn delete(url: impl IntoURL) -> Self { Self::new("DELETE".to_string(), url) } /// Create builder for a CONNECT request - pub fn connect(url: URL) -> Self { Self::new("CONNECT".to_string(), url) } + pub fn connect(url: impl IntoURL) -> Self { Self::new("CONNECT".to_string(), url) } /// Create builder for a OPTIONS request - pub fn options(url: URL) -> Self { Self::new("OPTIONS".to_string(), url) } + pub fn options(url: impl IntoURL) -> Self { Self::new("OPTIONS".to_string(), url) } /// Create builder for a TRACE request - pub fn trace(url: URL) -> Self { Self::new("TRACE".to_string(), url) } + pub fn trace(url: impl IntoURL) -> Self { Self::new("TRACE".to_string(), url) } /// Create builder for a PATCH request - pub fn patch(url: URL) -> Self { Self::new("PATCH".to_string(), url) } + pub fn patch(url: impl IntoURL) -> Self { Self::new("PATCH".to_string(), url) } /// Set request url - pub fn url(mut self, url: URL) -> Self { - self.url = url; + pub fn url(mut self, url: impl IntoURL) -> Self { + self.url = url.to_string(); self } @@ -108,7 +113,7 @@ impl RequestBuilder { /// Set query in url 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.url_query = Some(HashMap::from_iter(query.iter().map(|o| (o.0.to_string(), o.1.to_string())))); self } @@ -119,13 +124,24 @@ impl RequestBuilder { } /// Build request - pub fn build(self) -> HttpRequest { - HttpRequest { - url: self.url, + pub fn build(self) -> Result { + let mut url = self.url.to_url()?; + if let Some(query) = self.url_query { + url.query = query; + } + + Ok(HttpRequest { + url, method: self.method, - addr: "localhost:80".to_socket_addrs().unwrap().next().unwrap(), + addr: None, headers: self.headers, body: self.body.unwrap_or(Body::default()) - } + }) + } +} + +impl IntoRequest for RequestBuilder { + fn to_request(self) -> Result { + self.build() } } \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index e8b0f52..924f432 100755 --- a/src/error.rs +++ b/src/error.rs @@ -18,7 +18,8 @@ pub enum HttpError { ConnectError, ShutdownError, SslError, - UnknownScheme + UnknownScheme, + UrlNeedsRootError } impl std::fmt::Display for HttpError { diff --git a/src/request.rs b/src/request.rs index 5ef1c07..68dd154 100755 --- a/src/request.rs +++ b/src/request.rs @@ -6,39 +6,70 @@ use std::{ use async_trait::async_trait; use tokio::io::{AsyncReadExt, AsyncWrite, AsyncWriteExt}; -/// Request URL + +/// Request URL root (scheme://domain:port) +#[derive(Clone, Debug)] +pub struct RootURL { + pub scheme: String, + pub domain: String, + pub port: u16 +} + +impl FromStr for RootURL { + type Err = HttpError; + + fn from_str(text: &str) -> Result { + let (scheme, host) = text.split_once("://").ok_or(HttpError::UrlError)?; + let (domain, port) = host.split_once(":").unwrap_or(match scheme { + "https" => (host, "443"), + "http" => (host, "80"), + _ => { return Err(HttpError::UrlError) } + }); + let port = port.parse::().or(Err(HttpError::UrlError))?; + let scheme= scheme.to_string(); + let domain= domain.to_string(); + Ok(RootURL { scheme, domain, port }) + } +} + +impl ToString for RootURL { + fn to_string(&self) -> String { + format!("{}://{}", self.scheme, { + if (self.scheme == "http" && self.port == 80) || + (self.scheme == "https" && self.port == 443) { + format!("{}", self.domain) + } else { + format!("{}:{}", self.domain, self.port) + } + }) + } +} + +/// Request URL ({root}/path?query_key=query_value#anchor) #[derive(Clone, Debug)] pub struct URL { + pub root: Option, pub path: String, - pub domain: String, pub anchor: Option, - pub query: HashMap, - pub scheme: String, - pub port: u16 + pub query: HashMap } impl URL { pub fn new( - domain: String, - port: u16, + root: Option, path: String, anchor: Option, query: HashMap, - scheme: String ) -> URL { URL { path, - domain, anchor, query, - scheme, - port + root } } - /// Turns URL object to url string without scheme, domain, port - /// Example: /123.html?k=v#anc - pub fn to_path_string(&self) -> String { + fn to_path_str(&self) -> String { format!("{}{}{}", self.path, if self.query.is_empty() { String::new() } else { @@ -52,79 +83,109 @@ impl URL { }) } - /// Turns string without scheme, domain, port to URL object - /// Example of string: /123.html?k=v#anc - pub fn from_path_string(s: &str, scheme: String, domain: String, port: u16) -> Option { - let (s, anchor) = s.split_once("#").unwrap_or((s, "")); - let (path, query) = s.split_once("?").unwrap_or((s, "")); + fn from_path_str(text: &str) -> Option { + let (text, anchor) = text.split_once("#").unwrap_or((text, "")); + let (path, query) = text.split_once("?").unwrap_or((text, "")); + let path = path.to_string(); - let anchor = if anchor.is_empty() { None } else { Some(anchor.to_string()) }; - let query = if query.is_empty() { HashMap::new() } else { { + let anchor = if anchor.is_empty() { + None + } else { + Some(anchor.to_string()) + }; + + let query = if query.is_empty() { + HashMap::new() + } else { HashMap::from_iter(query.split("&").filter_map(|entry| { let (key, value) = entry.split_once("=").unwrap_or((entry, "")); Some((urlencoding::decode(key).ok()?.to_string(), urlencoding::decode(value).ok()?.to_string())) })) - } }; - let path = path.to_string(); - let scheme = scheme.to_string(); - Some(URL { path, domain, anchor, query, scheme, port }) + }; + + Some(URL { root: None, path, anchor, query }) } } impl FromStr for URL { type Err = HttpError; - /// Turns url string to URL object - /// Example: https://domain.com:999/123.html?k=v#anc - /// Example 2: http://exampl.eu/sing - fn from_str(s: &str) -> Result { - let (scheme, s) = s.split_once("://").ok_or(HttpError::UrlError)?; - let (host, s) = s.split_once("/").unwrap_or((s, "")); - let (domain, port) = host.split_once(":").unwrap_or((host, - if scheme == "http" { "80" } - else if scheme == "https" { "443" } - else { return Err(HttpError::UrlError) } - )); - let port = port.parse::().map_err(|_| HttpError::UrlError)?; - let (s, anchor) = s.split_once("#").unwrap_or((s, "")); - let (path, query) = s.split_once("?").unwrap_or((s, "")); + fn from_str(text: &str) -> Result { + if text.starts_with("/") { + return Self::from_path_str(text).ok_or(HttpError::UrlError) + } - let anchor = if anchor.is_empty() { None } else { Some(anchor.to_string()) }; - let query = if query.is_empty() { HashMap::new() } else { { - HashMap::from_iter(query.split("&").filter_map(|entry| { - let (key, value) = entry.split_once("=").unwrap_or((entry, "")); - Some((urlencoding::decode(key).ok()?.to_string(), urlencoding::decode(value).ok()?.to_string())) - })) - } }; - let domain = domain.to_string(); - let path = format!("/{path}"); - let scheme = scheme.to_string(); - Ok(URL { path, domain, anchor, query, scheme, port }) + let (scheme_n_host, path) = match text.split_once("://") { + Some((scheme, host_n_path)) => { + match host_n_path.split_once("/") { + Some((host, path)) => { + (format!("{}://{}", scheme, host), format!("/{}", path)) + }, None => { + (format!("{}://{}", scheme, host_n_path), "/".to_string()) + } + } + }, None => { + return Err(HttpError::UrlError) + } + }; + + let mut url = Self::from_path_str(&path).ok_or(HttpError::UrlError)?; + url.root = Some(RootURL::from_str(&scheme_n_host)?); + + Ok(url) } } impl ToString for URL { - /// Turns URL object to string - /// Example: https://domain.com:999/123.html?k=v#anc - /// Example 2: http://exampl.eu/sing fn to_string(&self) -> String { - format!("{}://{}{}", self.scheme, { - if (self.scheme == "http" && self.port != 80) || (self.scheme == "https" && self.port != 443) { - format!("{}:{}", self.domain, self.port) - } else { - self.domain.clone() - } - }, self.to_path_string()) + format!("{}{}", self.root.clone().map(|o| o.to_string()).unwrap_or_default(), self.to_path_str()) } } +pub trait IntoURL: ToString { + fn to_url(self) -> Result; +} + +impl IntoURL for &String { + fn to_url(self) -> Result { + URL::from_str(&self) + } +} + +impl IntoURL for String { + fn to_url(self) -> Result { + URL::from_str(&self) + } +} + +impl IntoURL for &str { + fn to_url(self) -> Result { + URL::from_str(&self) + } +} + +impl IntoURL for URL { + fn to_url(self) -> Result { + Ok(self) + } +} + +pub trait IntoRequest { + fn to_request(self) -> Result; +} + +impl IntoRequest for HttpRequest { + fn to_request(self) -> Result { + Ok(self) + } +} /// Http request #[derive(Debug, Clone)] pub struct HttpRequest { pub url: URL, pub method: String, - pub addr: SocketAddr, + pub addr: Option, pub headers: Headers, pub body: Body } @@ -138,19 +199,19 @@ impl Display for HttpRequest { impl HttpRequest { /// Create new http request pub fn new( - url: URL, + url: impl IntoURL, method: String, - addr: SocketAddr, headers: Headers, - body: Body - ) -> Self { - HttpRequest { - url, + body: Body, + addr: Option + ) -> Result { + Ok(HttpRequest { + url: url.to_url()?, method, - addr, headers, - body - } + body, + addr + }) } /// Read http request from stream @@ -170,18 +231,13 @@ impl HttpRequest { let headers = Headers::recv(stream).await?; let body = Body::recv(stream, &headers).await?; - Ok(HttpRequest::new( - URL::from_path_string( - &page, - "http".to_string(), - "localhost".to_string(), - 80 - ).ok_or(HttpError::UrlError)?, + HttpRequest::new( + page, method, - addr.clone(), headers, - body - )) + body, + Some(addr.clone()) + ) } /// Get multipart parts (requires Content-Type header) @@ -214,10 +270,13 @@ impl Sendable for HttpRequest { &self, stream: &mut (dyn AsyncWrite + Unpin + Send + Sync), ) -> Result<(), HttpError> { + let mut url = self.url.clone(); + url.root = None; + let mut head: String = String::new(); head.push_str(&self.method); head.push_str(" "); - head.push_str(&self.url.to_path_string()); + head.push_str(&url.to_string()); head.push_str(" HTTP/1.1"); head.push_str("\r\n"); stream.write_all(head.as_bytes()).await.map_err(|_| HttpError::WriteHeadError)?;