use std::{env, error::Error, sync::Arc};
use io_http::{
client::{HttpClientAsync, HttpClientError},
coroutine::*,
rfc9110::{
request::HttpRequest,
send::{HttpSendOutput, HttpSendYield},
},
};
use log::info;
use rustls::ClientConfig;
use rustls_platform_verifier::ConfigVerifierExt;
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
};
use tokio_rustls::{TlsConnector, client::TlsStream};
use url::Url;
const READ_BUFFER_SIZE: usize = 16 * 1024;
const MAX_REDIRECTS: usize = 8;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
env_logger::init();
let mut url: Url = match env::var("URL") {
Ok(url) => url.parse()?,
Err(_) => "https://example.com/".parse()?,
};
rustls::crypto::ring::default_provider()
.install_default()
.ok();
let config = Arc::new(ClientConfig::with_platform_verifier()?);
let connector = TlsConnector::from(config);
let mut response = None;
for _ in 0..MAX_REDIRECTS {
let domain = url.domain().ok_or("URL has no domain")?.to_owned();
let mut client = HttpClientTokio::connect(&connector, &domain, &url).await?;
let request = HttpRequest::get(url.clone())
.header("Host", &domain)
.header("Connection", "close");
let sent = tokio::spawn(async move { client.send(request).await }).await?;
match sent {
Ok(HttpSendOutput { response: res, .. }) => {
response = Some(res);
break;
}
Err(HttpClientError::UnexpectedRedirect { url: target, code }) => {
info!("redirected to {target} ({code})");
url = target;
}
Err(err) => return Err(err.into()),
}
}
let response = response.ok_or("too many redirects")?;
println!("{} {}", response.version, *response.status);
for (key, val) in &response.headers {
println!("{key}: {val}");
}
print!("{}", String::from_utf8_lossy(&response.body));
Ok(())
}
struct HttpClientTokio {
stream: TlsStream<TcpStream>,
}
impl HttpClientTokio {
async fn connect(
connector: &TlsConnector,
domain: &str,
url: &Url,
) -> Result<Self, Box<dyn Error>> {
let port = url.port_or_known_default().unwrap_or(443);
let tcp = TcpStream::connect((domain, port)).await?;
let name = domain.to_owned().try_into()?;
Ok(Self {
stream: connector.connect(name, tcp).await?,
})
}
}
impl HttpClientAsync for HttpClientTokio {
#[allow(clippy::manual_async_fn)]
fn run<C, T, E>(
&mut self,
mut coroutine: C,
) -> impl Future<Output = Result<T, HttpClientError>> + Send
where
C: HttpCoroutine<Yield = HttpYield, Return = Result<T, E>> + Send,
T: Send,
E: Send,
HttpClientError: From<E>,
{
async move {
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
HttpCoroutineState::Complete(Ok(out)) => return Ok(out),
HttpCoroutineState::Complete(Err(err)) => return Err(err.into()),
HttpCoroutineState::Yielded(HttpYield::WantsRead) => {
let n = self.stream.read(&mut buf).await?;
arg = Some(&buf[..n]);
}
HttpCoroutineState::Yielded(HttpYield::WantsWrite(bytes)) => {
self.stream.write_all(&bytes).await?;
arg = None;
}
}
}
}
}
#[allow(clippy::manual_async_fn)]
fn run_send<C, E>(
&mut self,
mut coroutine: C,
) -> impl Future<Output = Result<HttpSendOutput, HttpClientError>> + Send
where
C: HttpCoroutine<Yield = HttpSendYield, Return = Result<HttpSendOutput, E>> + Send,
E: Send,
HttpClientError: From<E>,
{
async move {
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
HttpCoroutineState::Complete(Ok(out)) => return Ok(out),
HttpCoroutineState::Complete(Err(err)) => return Err(err.into()),
HttpCoroutineState::Yielded(HttpSendYield::WantsRead) => {
let n = self.stream.read(&mut buf).await?;
arg = Some(&buf[..n]);
}
HttpCoroutineState::Yielded(HttpSendYield::WantsWrite(bytes)) => {
self.stream.write_all(&bytes).await?;
arg = None;
}
HttpCoroutineState::Yielded(HttpSendYield::WantsRedirect {
url,
response,
..
}) => {
return Err(HttpClientError::UnexpectedRedirect {
url,
code: *response.status,
});
}
}
}
}
}
}