ezhttp 0.2.2

Simple async http library with client and server
Documentation
use std::{pin::Pin, time::Duration};

use base64::Engine;
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
use tokio::{io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, net::TcpStream};
use tokio_io_timeout::TimeoutStream;
use tokio_openssl::SslStream;
use tokio_socks::tcp::{Socks4Stream, Socks5Stream};

use super::{error::HttpError, gen_multipart_boundary, headers::Headers, prelude::HttpResponse, request::HttpRequest, Sendable};

use base64::prelude::BASE64_STANDARD;

pub mod req_builder;
pub mod client;
pub mod proxy;

pub use req_builder::*;
pub use client::*;
pub use proxy::*;

trait RequestStream: AsyncRead + AsyncWrite + Unpin + Send + Sync {}
impl<T: AsyncRead + AsyncWrite + Unpin + Send + Sync> RequestStream for T {}

async fn connect_stream(proxy: Proxy, site_host: &str) -> Result<Box<dyn RequestStream>, HttpError> {
    Ok(match proxy {
        Proxy::Http { host, auth } | Proxy::Https { host, auth } => {
            let mut stream = TcpStream::connect(host).await.map_err(|_| HttpError::ConnectError)?;
            let auth_header = auth.map(|(u, p)| format!("Proxy-Authorization: basic {}\r\n", BASE64_STANDARD.encode(format!("{u}:{p}"))));
            let connect_request = format!("CONNECT {site_host} HTTP/1.1\r\nHost: {site_host}\r\n{}\r\n", auth_header.unwrap_or_default());
            stream.write_all(connect_request.as_bytes()).await.map_err(|_| HttpError::ConnectError)?;
            HttpResponse::recv(&mut stream).await.map_err(|_| HttpError::ConnectError)?;
            Box::new(stream)
        }
        Proxy::Socks4 { host, user } => Box::new(match user {
            Some(user) => Socks4Stream::connect_with_userid(host, site_host, &user).await.map_err(|_| HttpError::ConnectError)?,
            None => Socks4Stream::connect(host, site_host).await.map_err(|_| HttpError::ConnectError)?,
        }),
        Proxy::Socks5 { host, auth } => Box::new(match auth {
            Some((u, p)) => Socks5Stream::connect_with_password(host, site_host, &u, &p).await.map_err(|_| HttpError::ConnectError)?,
            None => Socks5Stream::connect(host, site_host).await.map_err(|_| HttpError::ConnectError)?,
        }),
        Proxy::None => Box::new(TcpStream::connect(site_host).await.map_err(|_| HttpError::ConnectError)?),
    })
}

async fn send_request(
    mut request: HttpRequest, ssl_verify: bool, proxy: Proxy, headers: Headers,
    connect_timeout: Option<Duration>, write_timeout: Option<Duration>, read_timeout: Option<Duration>
) -> Result<HttpResponse, HttpError> {
    for (key, value) in headers.entries() {
        request.headers.put_default(key, value);
    }
    request.headers.put_default("Connection", "close".to_string());
    request.headers.put_default("Host", request.url.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 stream: Box<dyn RequestStream> = match connect_timeout {
        Some(connect_timeout) => {
            tokio::time::timeout(
                connect_timeout,
                connect_stream(proxy, &site_host)
            ).await.map_err(|_| HttpError::ConnectError)??
        }, None => {
            connect_stream(proxy, &site_host).await?
        }
    };
    
    let mut stream = TimeoutStream::new(stream);
    stream.set_write_timeout(write_timeout);
    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?;
        request.send(&mut stream).await?;
        Ok(HttpResponse::recv(&mut stream).await?)
    } else {
        request.send(&mut stream).await?;
        Ok(HttpResponse::recv(&mut stream).await?)
    }
}

async fn ssl_wrapper<S: AsyncReadExt + AsyncWriteExt>(ssl_verify: bool, domain: String, stream: S) -> Result<Pin<Box<SslStream<S>>>, 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)
}