url root and ip now optional'
This commit is contained in:
parent
ce52997bed
commit
3726a28286
@ -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);
|
||||
|
||||
|
@ -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())
|
||||
|
@ -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(
|
||||
|
@ -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(),
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -18,7 +18,8 @@ pub enum HttpError {
|
||||
ConnectError,
|
||||
ShutdownError,
|
||||
SslError,
|
||||
UnknownScheme
|
||||
UnknownScheme,
|
||||
UrlNeedsRootError
|
||||
}
|
||||
|
||||
impl std::fmt::Display for HttpError {
|
||||
|
221
src/request.rs
221
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<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)?;
|
||||
|
Loading…
Reference in New Issue
Block a user