use std::{fmt, time::Duration, ops::Range, io};
#[derive(Clone, Default)]
pub enum Method {
#[default]
Get,
Post,
Put,
Delete,
Patch,
Head,
Options,
Trace,
}
#[derive(Clone, Copy, Default)]
pub enum Mode {
#[default]
Plain,
#[cfg(feature = "tls")]
Secure,
}
#[derive(Clone, Default)]
pub struct Uri<'a> {
pub host: &'a str,
pub path: &'a str,
}
#[derive(Clone)]
pub struct Query<'a> {
pub name: &'a str,
pub value: &'a str,
}
#[derive(Clone)]
pub struct Header<'a> {
pub name: &'a str,
pub value: &'a str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ReqId {
pub inner: usize,
}
#[derive(Default, Clone)]
pub struct RequestBuilder<'a> {
request: Request<'a>, }
impl<'a> RequestBuilder<'a> {
#[inline(always)]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.request.timeout = Some(timeout);
self
}
#[inline(always)]
pub fn method(mut self, method: Method) -> Self {
self.request.method = method;
self
}
#[cfg(feature = "tls")]
#[inline(always)]
pub fn secure(mut self) -> Self {
self.request.mode = Mode::Secure;
self
}
#[cfg(feature = "tls")]
#[inline(always)]
pub fn https(self) -> Self {
self.secure()
}
#[inline(always)]
pub fn host(mut self, host: &'a str) -> Self {
self.request.uri.host = host;
self
}
#[inline(always)]
pub fn path(mut self, path: &'a str) -> Self {
self.request.uri.path = path;
self
}
#[inline(always)]
pub fn query(mut self, name: &'a str, value: &'a str) -> Self {
self.request.queries.push(Query { name, value });
self
}
#[inline(always)]
pub fn set(mut self, name: &'a str, value: &'a str) -> Self {
self.request.headers.push(Header { name, value });
self
}
#[inline(always)]
pub fn header(self, name: &'a str, value: &'a str) -> Self {
self.set(name, value)
}
#[inline(always)]
pub fn cookie(mut self, name: &'a str, value: &'a str) -> Self {
self.request.cookies.push(Header { name, value });
self
}
#[inline(always)]
pub fn user_agent(self, value: &'a str) -> Self {
self.set("User-Agent", value)
}
#[inline(always)]
pub fn send<T: AsRef<[u8]> + ?Sized>(mut self, body: &'a T) -> Self {
self.request.body = body.as_ref();
self
}
#[inline(always)]
pub fn finish(self) -> Request<'a> {
self.request
}
}
impl<'a> From<RequestBuilder<'a>> for RawRequest {
#[inline(always)]
fn from(builder: RequestBuilder<'a>) -> Self {
builder.finish().format()
}
}
impl<'a> From<Request<'a>> for RawRequest {
#[inline(always)]
fn from(request: Request<'a>) -> Self {
request.format()
}
}
#[derive(Clone, Default)]
pub struct Request<'a> {
pub timeout: Option<Duration>,
pub method: Method,
pub mode: Mode,
pub uri: Uri<'a>,
pub queries: Vec<Query<'a>>,
pub headers: Vec<Header<'a>>,
pub cookies: Vec<Header<'a>>,
pub body: &'a [u8],
}
impl<'a> Request<'a> {
pub fn build() -> RequestBuilder<'a> {
RequestBuilder::default()
}
pub fn get() -> RequestBuilder<'a> {
RequestBuilder::default().method(Method::Get)
}
pub fn post() -> RequestBuilder<'a> {
RequestBuilder::default().method(Method::Post)
}
pub fn format(&self) -> RawRequest {
let method = match self.method {
Method::Get => "GET",
Method::Post => "POST",
Method::Put => "PUT",
Method::Delete => "DELETE",
Method::Patch => "PATCH",
Method::Head => "HEAD",
Method::Options => "OPTIONS",
Method::Trace => "TRACE",
};
let host = self.uri.host;
let trimmed_path = self.uri.path.trim_start_matches("/");
let mut path_builder = trimmed_path.to_string();
for (idx, Query { name, value }) in self.queries.iter().enumerate() {
path_builder += if idx == 0 { "?" } else { "&" };
path_builder += name;
path_builder += "=";
path_builder += value;
}
let mut headers = String::new();
let mut overwrite_encoding = false;
headers += "Content-Length: ";
headers += &self.body.len().to_string();
headers += "\r\n";
headers += "Connection: close";
headers += "\r\n";
for Header { name, value } in self.headers.iter() {
if *name == "Connection" || *name == "Content-Length" {
panic!("The `{}` header is managed by rtv, for more info see the `Request` documentation", name);
}
else if *name == "Accept-Encoding" { overwrite_encoding = true }
headers += name;
headers += ": ";
headers += value;
headers += "\r\n";
}
headers += "Cookie: ";
for Header { name, value } in self.cookies.iter() {
headers += name;
headers += "=";
headers += value;
headers += "; ";
}
headers += "\n";
if !overwrite_encoding {
headers += "Accept-Encoding: identity";
headers += "\r\n";
}
let head = format!("{} /{} HTTP/1.1\r\nHost: {}\r\n{}\r\n", method, path_builder, host, headers);
let host_idx = head.find("Host: ").unwrap() + 6;
let mut bytes = head.into_bytes();
bytes.extend_from_slice(self.body);
RawRequest {
bytes,
mode: self.mode,
timeout: self.timeout,
host: host_idx .. host_idx + self.uri.host.len()
}
}
}
pub struct RawRequest {
pub bytes: Vec<u8>,
pub mode: Mode,
pub timeout: Option<Duration>,
host: Range<usize>, }
impl RawRequest {
pub fn host(&self) -> &str {
std::str::from_utf8(
&self.bytes[self.host.clone()]
).unwrap()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OwnedHeader {
pub name: String,
pub value: String,
}
impl From<&httparse::Header<'_>> for OwnedHeader {
fn from(header: &httparse::Header) -> Self {
Self { name: header.name.to_string(), value: String::from_utf8_lossy(header.value).to_string() }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Status {
pub code: u16,
pub reason: String,
}
#[derive(Clone, PartialEq, Eq)]
pub struct ResponseHead {
pub status: Status,
pub headers: Vec<OwnedHeader>,
pub content_length: usize,
pub transfer_chunked: bool,
}
impl ResponseHead {
pub fn get_header<'d>(&'d self, name: &str) -> Option<&'d str> {
self.headers.iter().find_map(Self::match_header(name))
}
pub fn all_headers<'d>(&'d self, name: &'d str) -> impl Iterator<Item = &'d str> {
self.headers.iter().filter_map(Self::match_header(name))
}
fn match_header<'d>(name: &'d str) -> impl for<'e> Fn(&'e OwnedHeader) -> Option<&'e str> + 'd { move |header| if header.name == name { Some(&header.value[..]) } else { None }
}
}
impl fmt::Debug for ResponseHead {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
writeln!(f, "ResponseHead {{")?;
writeln!(f, " headers: [")?;
for header in self.headers.iter() {
writeln!(f, " {}: {}", header.name, header.value)?;
}
writeln!(f, " ]")?;
writeln!(f, " status: {:?}", self.status)?;
writeln!(f, " content_length: {:?}", self.content_length)?;
writeln!(f, " transfer_chunked: {:?}", self.transfer_chunked)?;
write!(f, "}}")?;
Ok(())
} else {
if self.transfer_chunked {
write!(f, "ResponseHead {{ status: {}: {}, transfer_chunked: true, ... }}",
self.status.code,
self.status.reason)
} else {
write!(f, "ResponseHead {{ status: {}: {}, content_length: {}, ... }}",
self.status.code,
self.status.reason,
self.content_length)
}
}
}
}
#[derive(Debug)]
pub struct Response {
pub id: ReqId,
pub state: ResponseState,
}
impl Response {
pub(crate) fn new(id_num: usize, state: ResponseState) -> Self {
Self { id: ReqId { inner: id_num }, state }
}
}
#[derive(PartialEq, Eq)]
pub enum ResponseState {
Head(ResponseHead),
Data(Vec<u8>),
Done,
TimedOut,
Aborted,
UnknownHost,
ProtocolError,
}
impl ResponseState {
pub fn is_finished(&self) -> bool {
self.is_done() || self.is_error()
}
pub fn is_done(&self) -> bool {
match self {
Self::Head(..) => false,
Self::Data(..) => false,
Self::Done => true, Self::TimedOut => false,
Self::Aborted => false,
Self::UnknownHost => false,
Self::ProtocolError => false,
}
}
pub fn is_error(&self) -> bool {
match self {
Self::Head(..) => false,
Self::Data(..) => false,
Self::Done => false,
Self::TimedOut => true, Self::Aborted => true, Self::UnknownHost => true, Self::ProtocolError => true, }
}
pub fn into_io_error(&self) -> Option<io::Error> {
match self {
ResponseState::Aborted => Some(io::Error::from(io::ErrorKind::ConnectionAborted)),
ResponseState::TimedOut => Some(io::Error::from(io::ErrorKind::TimedOut)),
ResponseState::UnknownHost => Some(io::Error::new(io::ErrorKind::Other, "unknown host")),
ResponseState::ProtocolError => Some(io::Error::new(io::ErrorKind::Other, "http protocol error")),
_other => None
}
}
}
impl fmt::Debug for ResponseState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TimedOut => write!(f, "TimedOut"),
Self::Head(head) => write!(f, "Head({:?})", head),
Self::Data(data) => write!(f, "Data({} bytes)", data.len()),
Self::Done => write!(f, "Done"),
Self::Aborted => write!(f, "Dead"),
Self::UnknownHost => write!(f, "UnknownHost"),
Self::ProtocolError => write!(f, "Error"),
}
}
}