use std::convert::From;
use std::fmt::Display;
use std::io::{prelude::*, BufWriter};
use std::str;
#[cfg(feature = "compress")]
use http::header::ACCEPT_ENCODING;
use http::{
header::{HeaderValue, IntoHeaderName, CONNECTION, CONTENT_LENGTH, HOST},
status::StatusCode,
HeaderMap, HttpTryFrom, Method, Version,
};
use url::Url;
#[cfg(feature = "charsets")]
use crate::charsets::Charset;
use crate::error::{HttpError, HttpResult};
use crate::parsing::{parse_response, ResponseReader};
use crate::streams::BaseStream;
pub trait HttpTryInto<T> {
fn try_into(self) -> Result<T, http::Error>;
}
impl<T, U> HttpTryInto<U> for T
where
U: HttpTryFrom<T>,
http::Error: From<<U as http::HttpTryFrom<T>>::Error>,
{
fn try_into(self) -> Result<U, http::Error> {
let val = U::try_from(self)?;
Ok(val)
}
}
fn header_insert<H, V>(headers: &mut HeaderMap, header: H, value: V) -> HttpResult
where
H: IntoHeaderName,
V: HttpTryInto<HeaderValue>,
{
let value = value.try_into()?;
headers.insert(header, value);
Ok(())
}
fn header_append<H, V>(headers: &mut HeaderMap, header: H, value: V) -> HttpResult
where
H: IntoHeaderName,
V: HttpTryInto<HeaderValue>,
{
let value = value.try_into()?;
headers.append(header, value);
Ok(())
}
pub struct Request {
url: Url,
method: Method,
headers: HeaderMap,
body: Vec<u8>,
follow_redirects: bool,
#[cfg(feature = "charsets")]
pub(crate) default_charset: Option<Charset>,
#[cfg(feature = "compress")]
allow_compression: bool,
}
impl Request {
pub fn new(base_url: &str, method: Method) -> Request {
let url = Url::parse(base_url).expect("invalid url");
match method {
Method::CONNECT => panic!("CONNECT is not yet supported"),
_ => {}
}
Request {
url,
method: method,
headers: HeaderMap::new(),
body: Vec::new(),
follow_redirects: true,
#[cfg(feature = "charsets")]
default_charset: None,
#[cfg(feature = "compress")]
allow_compression: true,
}
}
pub fn get(base_url: &str) -> Request {
Request::new(base_url, Method::GET)
}
pub fn post(base_url: &str) -> Request {
Request::new(base_url, Method::POST)
}
pub fn put(base_url: &str) -> Request {
Request::new(base_url, Method::PUT)
}
pub fn delete(base_url: &str) -> Request {
Request::new(base_url, Method::DELETE)
}
pub fn head(base_url: &str) -> Request {
Request::new(base_url, Method::HEAD)
}
pub fn options(base_url: &str) -> Request {
Request::new(base_url, Method::OPTIONS)
}
pub fn patch(base_url: &str) -> Request {
Request::new(base_url, Method::PATCH)
}
pub fn trace(base_url: &str) -> Request {
Request::new(base_url, Method::TRACE)
}
pub fn param<V>(&mut self, key: &str, value: V)
where
V: Display,
{
self.url.query_pairs_mut().append_pair(key, &format!("{}", value));
}
pub fn header<H, V>(&mut self, header: H, value: V) -> HttpResult
where
H: IntoHeaderName,
V: HttpTryInto<HeaderValue>,
{
header_insert(&mut self.headers, header, value)
}
pub fn header_append<H, V>(&mut self, header: H, value: V) -> HttpResult
where
H: IntoHeaderName,
V: HttpTryInto<HeaderValue>,
{
header_append(&mut self.headers, header, value)
}
pub fn body(&mut self, body: impl AsRef<[u8]>) {
self.body = body.as_ref().to_owned();
}
pub fn follow_redirects(&mut self, follow_redirects: bool) {
self.follow_redirects = follow_redirects;
}
#[cfg(feature = "charsets")]
pub fn default_charset(&mut self, default_charset: Option<Charset>) {
self.default_charset = default_charset;
}
#[cfg(feature = "compress")]
pub fn allow_compression(&mut self, allow_compression: bool) {
self.allow_compression = allow_compression;
}
fn base_redirect_url(&self, location: &str, previous_url: &Url) -> HttpResult<Url> {
Ok(match Url::parse(location) {
Ok(url) => url,
Err(url::ParseError::RelativeUrlWithoutBase) => previous_url
.join(location)
.map_err(|_| HttpError::InvalidUrl("cannot join location with new url"))?,
Err(_) => Err(HttpError::InvalidUrl("invalid redirection url"))?,
})
}
pub fn send(mut self) -> HttpResult<(StatusCode, HeaderMap, ResponseReader)> {
let mut url = self.url.clone();
loop {
let mut stream = BaseStream::connect(&url)?;
self.write_request(&mut stream, &url)?;
let (status, headers, resp) = parse_response(stream, &self)?;
debug!("status code {}", status.as_u16());
if !self.follow_redirects || !status.is_redirection() {
return Ok((status, headers, resp));
}
let location = headers
.get(http::header::LOCATION)
.ok_or(HttpError::InvalidResponse("redirect has no location header"))?;
let location = location
.to_str()
.map_err(|_| HttpError::InvalidResponse("location to str error"))?;
let new_url = self.base_redirect_url(location, &url)?;
url = new_url;
debug!("redirected to {} giving url {}", location, url,);
}
}
fn write_request<W>(&mut self, writer: W, url: &Url) -> HttpResult
where
W: Write,
{
let mut writer = BufWriter::new(writer);
let version = Version::HTTP_11;
let has_body = !self.body.is_empty() && self.method != Method::TRACE;
if let Some(query) = url.query() {
debug!("{} {}?{} {:?}", self.method.as_str(), url.path(), query, version,);
write!(
writer,
"{} {}?{} {:?}\r\n",
self.method.as_str(),
url.path(),
query,
version,
)?;
} else {
debug!("{} {} {:?}", self.method.as_str(), url.path(), version);
write!(writer, "{} {} {:?}\r\n", self.method.as_str(), url.path(), version,)?;
}
header_insert(&mut self.headers, CONNECTION, "close")?;
let host = url.host_str().ok_or(HttpError::InvalidUrl("url has no host"))?;
if let Some(port) = url.port() {
header_insert(&mut self.headers, HOST, format!("{}:{}", host, port))?;
} else {
header_insert(&mut self.headers, HOST, host)?;
}
if has_body {
header_insert(&mut self.headers, CONTENT_LENGTH, format!("{}", self.body.len()))?;
}
self.compression_header()?;
self.write_headers(&mut writer)?;
if has_body {
debug!("writing out body of length {}", self.body.len());
writer.write_all(&self.body)?;
}
writer.flush()?;
Ok(())
}
fn write_headers<W>(&self, writer: &mut W) -> HttpResult
where
W: Write,
{
for (key, value) in self.headers.iter() {
write!(writer, "{}: ", key.as_str())?;
writer.write_all(value.as_bytes())?;
write!(writer, "\r\n")?;
}
write!(writer, "\r\n")?;
Ok(())
}
#[cfg(feature = "compress")]
fn compression_header(&mut self) -> HttpResult {
if self.allow_compression {
header_insert(&mut self.headers, ACCEPT_ENCODING, "gzip, deflate")?;
}
Ok(())
}
#[cfg(not(feature = "compress"))]
fn compression_header(&mut self) -> HttpResult {
Ok(())
}
}