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]
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()
.ssl_verify(false)
.connect_timeout(Duration::from_secs(5))
@ -12,7 +14,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
.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);

View File

@ -66,7 +66,7 @@ impl EzSite {
#[async_trait]
impl HttpServer for EzSite {
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 {
Some(resp.as_box())

View File

@ -6,7 +6,7 @@ struct EzSite(String);
#[async_trait]
impl HttpServer for EzSite {
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 == "/" {
Some(HttpResponse::new(

View File

@ -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<HttpResponse, HttpError> {
pub async fn send(&self, request: impl IntoRequest) -> Result<HttpResponse, HttpError> {
send_request(
request,
request.to_request()?,
self.ssl_verify,
self.proxy.clone(),
self.headers.clone(),

View File

@ -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<dyn RequestStream> = 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 {

View File

@ -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>
body: Option<Body>,
url_query: Option<HashMap<String, String>>
}
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<HttpRequest, HttpError> {
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<HttpRequest, HttpError> {
self.build()
}
}

View File

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

View File

@ -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<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)]
pub struct URL {
pub root: Option<RootURL>,
pub path: String,
pub domain: String,
pub anchor: Option<String>,
pub query: HashMap<String, String>,
pub scheme: String,
pub port: u16
pub query: HashMap<String, String>
}
impl URL {
pub fn new(
domain: String,
port: u16,
root: Option<RootURL>,
path: String,
anchor: Option<String>,
query: HashMap<String, String>,
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<Self> {
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<Self> {
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<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, ""));
fn from_str(text: &str) -> Result<Self, Self::Err> {
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<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
#[derive(Debug, Clone)]
pub struct HttpRequest {
pub url: URL,
pub method: String,
pub addr: SocketAddr,
pub addr: Option<SocketAddr>,
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<SocketAddr>
) -> Result<Self, HttpError> {
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)?;