httpio 0.2.4

A transport-agnostic, async HTTP/1.1 client library for any runtime.
Documentation
mod tls_connect;

use std::env;
use std::error::Error;
use std::time::Duration;

use crate::tls_connect::connect_rustls;
use httpio::enums::char_set::CharSet;
use httpio::enums::header::http_header::HttpHeader;
use httpio::enums::http_body::HttpBody;
use httpio::enums::http_request_method::HttpRequestMethod;
use httpio::enums::http_version::HttpVersion;
use httpio::structures::connection::http_connection::HttpConnection;
use httpio::structures::header::header_list::HttpHeaderList;
use httpio::utils::http_header_field_name;
use httpio::utils::url::Url;
use log::debug_with;
use log::enums::log_level::LogLevel;
use log::structs::logger::Logger;

use httpio::compression::set_compression_provider;

#[path = "../shared/mod.rs"]
mod shared;
use shared::Flate2Decoder;

const TARGET: &str = "https://www.example.com";
const USER_AGENT: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Safari/605.1.15";
const ACCEPT: &str = "text/html, application/json, */*;q=0.8";
const ACCEPT_LANGUAGE: &str =
    "en,es-ES;q=0.9,es;q=0.8,it;q=0.7,de;q=0.6,cs;q=0.5,ru;q=0.4,ja;q=0.3,uk;q=0.2";

const CONNECTION_TIMEOUT: Duration = Duration::from_secs(10);

fn main() -> Result<(), Box<dyn Error>> {
    set_compression_provider(Box::new(Flate2Decoder))
        .map_err(|e| format!("Failed to set compression provider: {}", e))?;

    let use_smol = env::args().any(|arg| arg == "--smol");

    if use_smol {
        println!("Using smol runtime");
        smol_main()
    } else {
        println!("Using async-foundation runtime");
        async_foundation_main()
    }
}

fn async_foundation_main() -> Result<(), Box<dyn Error>> {
    use async_foundation::net::tcp_stream::TcpStream;
    use futures::executor::block_on;

    block_on(async {
        let url: Url = TARGET.into();
        let addr = url.socket_address()?;
        let host_name = url.domain.to_string();
        let logger = logger("HTTP");

        debug_with!(logger,  "Connecting to {}...", addr);
        let stream = TcpStream::connect(addr)?;
        let (reader, writer) = 
            connect_rustls(stream, Some(host_name.clone())).await?;

        let mut connection = HttpConnection::new(
            reader,
            writer,
            HttpVersion::Http11,
            &host_name,
            request_headers().into(),
            logger,
        )?;
        connection
            .send_request(
                HttpRequestMethod::Get,
                &url.path_query(),
                HttpHeaderList::default(),
                HttpBody::None,
            )
            .await?;
        let message = connection.read_response().await?;
        let charset = message.headers.get_charset(CharSet::Iso88591);
        let _message: String = message.body.to_string(charset)?;
        Ok(())
    })
}

fn smol_main() -> Result<(), Box<dyn Error>> {
    use smol::net::TcpStream;
    use smol::Timer;
    use futures::future::{select, Either};

    smol::block_on(async {
        let url: Url = TARGET.into();
        let addr = url.socket_address()?;
        let host_name = url.domain.to_string();
        let logger = logger("HTTP");
        debug_with!(logger,  "Connecting to {}...", addr);
        
        let connect_future = Box::pin(TcpStream::connect(addr));
        let timeout_future = Box::pin(Timer::after(CONNECTION_TIMEOUT));
        
        let tcp_stream = match select(connect_future, timeout_future).await {
            Either::Left((Ok(stream), _)) => stream,
            Either::Left((Err(e), _)) => return Err(e.into()),
            Either::Right(_) => return Err(format!("TCP connection timed out after {}s", CONNECTION_TIMEOUT.as_secs()).into()),
        };
        
        let handshake_future = Box::pin(connect_rustls(tcp_stream, Some(host_name.clone())));
        let timeout_future = Box::pin(Timer::after(CONNECTION_TIMEOUT));

        let (reader, writer) = match select(handshake_future, timeout_future).await {
            Either::Left((Ok(res), _)) => res,
            Either::Left((Err(e), _)) => return Err(e.into()),
            Either::Right(_) => return Err(format!("TLS handshake timed out after {}s", CONNECTION_TIMEOUT.as_secs()).into()),
        };

        let mut connection = HttpConnection::new(
            reader,
            writer,
            HttpVersion::Http11,
            &host_name,
            request_headers().into(),
            logger,
        )?;
        connection
            .send_request(
                HttpRequestMethod::Get,
                &url.path_query(),
                HttpHeaderList::default(),
                HttpBody::None,
            )
            .await?;
        let message = connection.read_response().await?;
        let charset = message.headers.get_charset(CharSet::Iso88591);
        let _message: String = message.body.to_string(charset)?;
        Ok(())
    })
}

fn request_headers() -> Vec<HttpHeader> {
    vec![
        HttpHeader::new(http_header_field_name::USER_AGENT, USER_AGENT),
        HttpHeader::new(http_header_field_name::ACCEPT, ACCEPT),
        HttpHeader::Generic {
            key: "Cache-Control".to_string(),
            value: "no-cache".to_string(),
        },
        HttpHeader::Generic {
            key: "Accept-Language".to_string(),
            value: ACCEPT_LANGUAGE.to_string(),
        },
    ]
}

fn logger(_subject: &'static str) -> Logger {
    Logger::with_level(LogLevel::Debug)
        .stdout()
        .time_format("%H:%M:%S%.3f")
}