url root and ip now optional'

This commit is contained in:
MeexReay 2025-01-26 01:10:53 +03:00
parent ce52997bed
commit 3726a28286
8 changed files with 199 additions and 118 deletions

View File

@ -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] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
dbg!("https://meex.lol/dku?key=value#hex_id".to_url().unwrap().to_string());
let client = ClientBuilder::new() let client = ClientBuilder::new()
.ssl_verify(false) .ssl_verify(false)
.connect_timeout(Duration::from_secs(5)) .connect_timeout(Duration::from_secs(5))
@ -12,7 +14,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
.header("User-Agent", "EzHttp/0.1.0") .header("User-Agent", "EzHttp/0.1.0")
.build(); .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); println!("request: {:?}", &request);

View File

@ -66,7 +66,7 @@ impl EzSite {
#[async_trait] #[async_trait]
impl HttpServer for EzSite { impl HttpServer for EzSite {
async fn on_request(&self, req: &HttpRequest) -> Option<Box<dyn Sendable>> { async fn on_request(&self, req: &HttpRequest) -> Option<Box<dyn Sendable>> {
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 { if let Some(resp) = self.get_main_page(req).await {
Some(resp.as_box()) Some(resp.as_box())

View File

@ -6,7 +6,7 @@ struct EzSite(String);
#[async_trait] #[async_trait]
impl HttpServer for EzSite { impl HttpServer for EzSite {
async fn on_request(&self, req: &HttpRequest) -> Option<Box<dyn Sendable>> { async fn on_request(&self, req: &HttpRequest) -> Option<Box<dyn Sendable>> {
println!("{} > {} {}", req.addr, req.method, req.url.to_path_string()); println!("{} > {} {}", req.addr?, req.method, req.url.to_string());
if req.url.path == "/" { if req.url.path == "/" {
Some(HttpResponse::new( Some(HttpResponse::new(

View File

@ -1,6 +1,6 @@
use std::time::Duration; 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}; use super::{send_request, Proxy};
@ -107,9 +107,9 @@ 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: impl IntoRequest) -> Result<HttpResponse, HttpError> {
send_request( send_request(
request, request.to_request()?,
self.ssl_verify, self.ssl_verify,
self.proxy.clone(), self.proxy.clone(),
self.headers.clone(), self.headers.clone(),

View File

@ -51,11 +51,14 @@ async fn send_request(
for (key, value) in headers.entries() { for (key, value) in headers.entries() {
request.headers.put_default(key, value); 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("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()); 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<dyn RequestStream> = match connect_timeout { let stream: Box<dyn RequestStream> = match connect_timeout {
Some(connect_timeout) => { Some(connect_timeout) => {
tokio::time::timeout( tokio::time::timeout(
@ -72,8 +75,8 @@ async fn send_request(
stream.set_read_timeout(read_timeout); stream.set_read_timeout(read_timeout);
let mut stream = Box::pin(stream); let mut stream = Box::pin(stream);
if request.url.scheme == "https" { if root.scheme == "https" {
let mut stream = ssl_wrapper(ssl_verify, request.url.domain.clone(), stream).await?; let mut stream = ssl_wrapper(ssl_verify, root.domain.clone(), stream).await?;
request.send(&mut stream).await?; request.send(&mut stream).await?;
Ok(HttpResponse::recv(&mut stream).await?) Ok(HttpResponse::recv(&mut stream).await?)
} else { } else {

View File

@ -1,58 +1,63 @@
use std::{collections::HashMap, net::ToSocketAddrs}; use std::collections::HashMap;
use serde_json::Value; 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) /// Builder for [`HttpRequest`](HttpRequest)
#[derive(Debug, Clone)]
pub struct RequestBuilder { pub struct RequestBuilder {
method: String, method: String,
url: URL, url: String,
headers: Headers, headers: Headers,
body: Option<Body> body: Option<Body>,
url_query: Option<HashMap<String, String>>
} }
impl RequestBuilder { impl RequestBuilder {
/// Create builder with a custom method /// Create builder with a custom method
pub fn new(method: String, url: URL) -> Self { pub fn new(method: String, url: impl IntoURL) -> Self {
RequestBuilder { RequestBuilder {
method, method,
url, url: url.to_string(),
headers: Headers::new(), headers: Headers::new(),
body: None body: None,
url_query: None
} }
} }
/// Create builder for a GET request /// 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 /// 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 /// 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 /// 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 /// 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 /// 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 /// 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 /// 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 /// 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 /// Set request url
pub fn url(mut self, url: URL) -> Self { pub fn url(mut self, url: impl IntoURL) -> Self {
self.url = url; self.url = url.to_string();
self self
} }
@ -108,7 +113,7 @@ impl RequestBuilder {
/// Set query in url /// Set query in url
pub fn url_query(mut self, query: &[(impl ToString, impl ToString)]) -> 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.url_query = Some(HashMap::from_iter(query.iter().map(|o| (o.0.to_string(), o.1.to_string()))));
self self
} }
@ -119,13 +124,24 @@ impl RequestBuilder {
} }
/// Build request /// Build request
pub fn build(self) -> HttpRequest { pub fn build(self) -> Result<HttpRequest, HttpError> {
HttpRequest { let mut url = self.url.to_url()?;
url: self.url, if let Some(query) = self.url_query {
url.query = query;
}
Ok(HttpRequest {
url,
method: self.method, method: self.method,
addr: "localhost:80".to_socket_addrs().unwrap().next().unwrap(), addr: None,
headers: self.headers, headers: self.headers,
body: self.body.unwrap_or(Body::default()) body: self.body.unwrap_or(Body::default())
} })
}
}
impl IntoRequest for RequestBuilder {
fn to_request(self) -> Result<HttpRequest, HttpError> {
self.build()
} }
} }

View File

@ -18,7 +18,8 @@ pub enum HttpError {
ConnectError, ConnectError,
ShutdownError, ShutdownError,
SslError, SslError,
UnknownScheme UnknownScheme,
UrlNeedsRootError
} }
impl std::fmt::Display for HttpError { impl std::fmt::Display for HttpError {

View File

@ -6,39 +6,70 @@ use std::{
use async_trait::async_trait; use async_trait::async_trait;
use tokio::io::{AsyncReadExt, AsyncWrite, AsyncWriteExt}; 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<Self, Self::Err> {
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::<u16>().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)] #[derive(Clone, Debug)]
pub struct URL { pub struct URL {
pub root: Option<RootURL>,
pub path: String, pub path: String,
pub domain: String,
pub anchor: Option<String>, pub anchor: Option<String>,
pub query: HashMap<String, String>, pub query: HashMap<String, String>
pub scheme: String,
pub port: u16
} }
impl URL { impl URL {
pub fn new( pub fn new(
domain: String, root: Option<RootURL>,
port: u16,
path: String, path: String,
anchor: Option<String>, anchor: Option<String>,
query: HashMap<String, String>, query: HashMap<String, String>,
scheme: String
) -> URL { ) -> URL {
URL { URL {
path, path,
domain,
anchor, anchor,
query, query,
scheme, root
port
} }
} }
/// Turns URL object to url string without scheme, domain, port fn to_path_str(&self) -> String {
/// Example: /123.html?k=v#anc
pub fn to_path_string(&self) -> String {
format!("{}{}{}", self.path, if self.query.is_empty() { format!("{}{}{}", self.path, if self.query.is_empty() {
String::new() String::new()
} else { } else {
@ -52,79 +83,109 @@ impl URL {
}) })
} }
/// Turns string without scheme, domain, port to URL object fn from_path_str(text: &str) -> Option<Self> {
/// Example of string: /123.html?k=v#anc let (text, anchor) = text.split_once("#").unwrap_or((text, ""));
pub fn from_path_string(s: &str, scheme: String, domain: String, port: u16) -> Option<Self> { let (path, query) = text.split_once("?").unwrap_or((text, ""));
let (s, anchor) = s.split_once("#").unwrap_or((s, "")); let path = path.to_string();
let (path, query) = s.split_once("?").unwrap_or((s, ""));
let anchor = if anchor.is_empty() { None } else { Some(anchor.to_string()) }; let anchor = if anchor.is_empty() {
let query = if query.is_empty() { HashMap::new() } else { { None
} else {
Some(anchor.to_string())
};
let query = if query.is_empty() {
HashMap::new()
} else {
HashMap::from_iter(query.split("&").filter_map(|entry| { HashMap::from_iter(query.split("&").filter_map(|entry| {
let (key, value) = entry.split_once("=").unwrap_or((entry, "")); let (key, value) = entry.split_once("=").unwrap_or((entry, ""));
Some((urlencoding::decode(key).ok()?.to_string(), urlencoding::decode(value).ok()?.to_string())) 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 { root: None, path, anchor, query })
Some(URL { path, domain, anchor, query, scheme, port })
} }
} }
impl FromStr for URL { impl FromStr for URL {
type Err = HttpError; type Err = HttpError;
/// Turns url string to URL object fn from_str(text: &str) -> Result<Self, Self::Err> {
/// Example: https://domain.com:999/123.html?k=v#anc if text.starts_with("/") {
/// Example 2: http://exampl.eu/sing return Self::from_path_str(text).ok_or(HttpError::UrlError)
fn from_str(s: &str) -> Result<Self, Self::Err> { }
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::<u16>().map_err(|_| HttpError::UrlError)?;
let (s, anchor) = s.split_once("#").unwrap_or((s, ""));
let (path, query) = s.split_once("?").unwrap_or((s, ""));
let anchor = if anchor.is_empty() { None } else { Some(anchor.to_string()) }; let (scheme_n_host, path) = match text.split_once("://") {
let query = if query.is_empty() { HashMap::new() } else { { Some((scheme, host_n_path)) => {
HashMap::from_iter(query.split("&").filter_map(|entry| { match host_n_path.split_once("/") {
let (key, value) = entry.split_once("=").unwrap_or((entry, "")); Some((host, path)) => {
Some((urlencoding::decode(key).ok()?.to_string(), urlencoding::decode(value).ok()?.to_string())) (format!("{}://{}", scheme, host), format!("/{}", path))
})) }, None => {
} }; (format!("{}://{}", scheme, host_n_path), "/".to_string())
let domain = domain.to_string(); }
let path = format!("/{path}"); }
let scheme = scheme.to_string(); }, None => {
Ok(URL { path, domain, anchor, query, scheme, port }) 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 { 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 { fn to_string(&self) -> String {
format!("{}://{}{}", self.scheme, { format!("{}{}", self.root.clone().map(|o| o.to_string()).unwrap_or_default(), self.to_path_str())
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())
} }
} }
pub trait IntoURL: ToString {
fn to_url(self) -> Result<URL, HttpError>;
}
impl IntoURL for &String {
fn to_url(self) -> Result<URL, HttpError> {
URL::from_str(&self)
}
}
impl IntoURL for String {
fn to_url(self) -> Result<URL, HttpError> {
URL::from_str(&self)
}
}
impl IntoURL for &str {
fn to_url(self) -> Result<URL, HttpError> {
URL::from_str(&self)
}
}
impl IntoURL for URL {
fn to_url(self) -> Result<URL, HttpError> {
Ok(self)
}
}
pub trait IntoRequest {
fn to_request(self) -> Result<HttpRequest, HttpError>;
}
impl IntoRequest for HttpRequest {
fn to_request(self) -> Result<HttpRequest, HttpError> {
Ok(self)
}
}
/// Http request /// Http request
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct HttpRequest { pub struct HttpRequest {
pub url: URL, pub url: URL,
pub method: String, pub method: String,
pub addr: SocketAddr, pub addr: Option<SocketAddr>,
pub headers: Headers, pub headers: Headers,
pub body: Body pub body: Body
} }
@ -138,19 +199,19 @@ impl Display for HttpRequest {
impl HttpRequest { impl HttpRequest {
/// Create new http request /// Create new http request
pub fn new( pub fn new(
url: URL, url: impl IntoURL,
method: String, method: String,
addr: SocketAddr,
headers: Headers, headers: Headers,
body: Body body: Body,
) -> Self { addr: Option<SocketAddr>
HttpRequest { ) -> Result<Self, HttpError> {
url, Ok(HttpRequest {
url: url.to_url()?,
method, method,
addr,
headers, headers,
body body,
} addr
})
} }
/// Read http request from stream /// Read http request from stream
@ -170,18 +231,13 @@ impl HttpRequest {
let headers = Headers::recv(stream).await?; let headers = Headers::recv(stream).await?;
let body = Body::recv(stream, &headers).await?; let body = Body::recv(stream, &headers).await?;
Ok(HttpRequest::new( HttpRequest::new(
URL::from_path_string( page,
&page,
"http".to_string(),
"localhost".to_string(),
80
).ok_or(HttpError::UrlError)?,
method, method,
addr.clone(),
headers, headers,
body body,
)) Some(addr.clone())
)
} }
/// Get multipart parts (requires Content-Type header) /// Get multipart parts (requires Content-Type header)
@ -214,10 +270,13 @@ impl Sendable for HttpRequest {
&self, &self,
stream: &mut (dyn AsyncWrite + Unpin + Send + Sync), stream: &mut (dyn AsyncWrite + Unpin + Send + Sync),
) -> Result<(), HttpError> { ) -> Result<(), HttpError> {
let mut url = self.url.clone();
url.root = None;
let mut head: String = String::new(); let mut head: String = String::new();
head.push_str(&self.method); head.push_str(&self.method);
head.push_str(" "); 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(" HTTP/1.1");
head.push_str("\r\n"); head.push_str("\r\n");
stream.write_all(head.as_bytes()).await.map_err(|_| HttpError::WriteHeadError)?; stream.write_all(head.as_bytes()).await.map_err(|_| HttpError::WriteHeadError)?;