use crate::connection::Connection;
use std::collections::HashMap;
use std::fmt;
use std::io::Error;
pub type URL = String;
#[derive(Clone, PartialEq, Debug)]
pub enum Method {
Get,
Head,
Post,
Put,
Delete,
Connect,
Options,
Trace,
Patch,
Custom(String),
}
impl fmt::Display for Method {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Method::Get => write!(f, "GET"),
Method::Head => write!(f, "HEAD"),
Method::Post => write!(f, "POST"),
Method::Put => write!(f, "PUT"),
Method::Delete => write!(f, "DELETE"),
Method::Connect => write!(f, "CONNECT"),
Method::Options => write!(f, "OPTIONS"),
Method::Trace => write!(f, "TRACE"),
Method::Patch => write!(f, "PATCH"),
Method::Custom(ref s) => write!(f, "{}", s),
}
}
}
pub struct Request {
pub(crate) method: Method,
pub(crate) host: URL,
resource: URL,
headers: HashMap<String, String>,
body: Option<String>,
pub(crate) timeout: Option<u64>,
https: bool,
pub(crate) redirects: Vec<URL>,
}
impl Request {
pub fn new<T: Into<URL>>(method: Method, url: T) -> Request {
let (host, resource, https) = parse_url(url.into());
Request {
method,
host,
resource,
headers: HashMap::new(),
body: None,
timeout: None,
https,
redirects: Vec::new(),
}
}
pub fn with_header<T: Into<String>, U: Into<String>>(mut self, key: T, value: U) -> Request {
self.headers.insert(key.into(), value.into());
self
}
pub fn with_body<T: Into<String>>(mut self, body: T) -> Request {
let body = body.into();
let body_length = body.len();
self.body = Some(body);
self.with_header("Content-Length", format!("{}", body_length))
}
pub fn with_timeout(mut self, timeout: u64) -> Request {
self.timeout = Some(timeout);
self
}
#[cfg(feature = "https")]
pub fn send(self) -> Result<Response, Error> {
if self.https {
Connection::new(self).send_https()
} else {
Connection::new(self).send()
}
}
#[cfg(not(feature = "https"))]
pub fn send(self) -> Result<Response, Error> {
if self.https {
panic!("Can't send requests to urls that start with https:// when the `https` feature is not enabled!")
} else {
Connection::new(self).send()
}
}
pub(crate) fn to_string(&self) -> String {
let mut http = String::new();
http += &format!(
"{} {} HTTP/1.1\r\nHost: {}\r\n",
self.method, self.resource, self.host
);
for (k, v) in &self.headers {
http += &format!("{}: {}\r\n", k, v);
}
http += "\r\n";
if let Some(ref body) = &self.body {
http += body;
}
http
}
pub(crate) fn redirect_to(&mut self, url: URL) {
self.redirects
.push(create_url(&self.host, &self.resource, self.https));
let (host, resource, https) = parse_url(url);
self.host = host;
self.resource = resource;
self.https = https;
}
}
pub struct Response {
pub status_code: i32,
pub reason_phrase: String,
pub headers: HashMap<String, String>,
pub body: String,
pub body_bytes: Vec<u8>,
}
impl Response {
pub(crate) fn from_bytes(bytes: Vec<u8>) -> Response {
let (status_code, reason_phrase) = parse_status_line(&bytes);
let (headers, body_bytes) = parse_http_response_content(&bytes);
Response {
status_code,
reason_phrase,
headers,
body: std::str::from_utf8(&body_bytes).unwrap_or("").to_owned(),
body_bytes,
}
}
}
fn create_url(host: &str, resource: &str, https: bool) -> URL {
let prefix = if https { "https://" } else { "http://" };
return format!("{}{}{}", prefix, host, resource);
}
fn parse_url(url: URL) -> (URL, URL, bool) {
let mut first = URL::new();
let mut second = URL::new();
let mut slashes = 0;
for c in url.chars() {
if c == '/' {
slashes += 1;
} else if slashes == 2 {
first.push(c);
}
if slashes >= 3 {
second.push(c);
}
}
if second.is_empty() {
second += "/";
}
let https = url.starts_with("https://");
if !first.contains(':') {
first += if https { ":443" } else { ":80" };
}
(first, second, https)
}
pub(crate) fn parse_status_line(http_response: &[u8]) -> (i32, String) {
let (line, _) = split_at(http_response, "\r\n");
if let Ok(line) = std::str::from_utf8(line) {
let mut split = line.split(' ');
if let Some(code) = split.nth(1) {
if let Ok(code) = code.parse::<i32>() {
if let Some(reason) = split.next() {
return (code, reason.to_string());
}
}
}
}
(503, "Server did not provide a status line".to_string())
}
fn parse_http_response_content(http_response: &[u8]) -> (HashMap<String, String>, Vec<u8>) {
let (headers_text, body) = split_at(http_response, "\r\n\r\n");
let mut headers = HashMap::new();
let mut status_line = true;
if let Ok(headers_text) = std::str::from_utf8(headers_text) {
for line in headers_text.lines() {
if status_line {
status_line = false;
continue;
} else if let Some((key, value)) = parse_header(line) {
headers.insert(key, value);
}
}
}
(headers, body.to_vec())
}
fn split_at<'a>(bytes: &'a [u8], splitter: &str) -> (&'a [u8], &'a [u8]) {
for i in 0..bytes.len() - splitter.len() {
if let Ok(s) = std::str::from_utf8(&bytes[i..i + splitter.len()]) {
if s == splitter {
return (&bytes[..i], &bytes[i + splitter.len()..]);
}
}
}
(bytes, &[])
}
pub(crate) fn parse_header(line: &str) -> Option<(String, String)> {
if let Some(index) = line.find(':') {
let key = line[..index].trim().to_string();
let value = line[(index + 1)..].trim().to_string();
Some((key, value))
} else {
None
}
}