use url::{self, Url};
use super::constants;
use super::results::{CabotError, CabotResult};
#[derive(Default)]
pub struct Request {
host: String,
port: u16,
authority: String,
is_domain: bool,
scheme: String,
http_method: String,
request_uri: String,
http_version: String,
headers: Vec<String>,
body: Option<Vec<u8>>,
}
impl Request {
fn new(
host: String,
port: u16,
authority: String,
is_domain: bool,
scheme: String,
http_method: String,
request_uri: String,
http_version: String,
headers: Vec<String>,
body: Option<Vec<u8>>,
) -> Request {
Request {
host,
port,
authority,
is_domain,
scheme,
http_method,
request_uri,
http_version,
headers,
body,
}
}
pub fn http_method(&self) -> &str {
self.http_method.as_str()
}
pub fn body(&self) -> Option<&[u8]> {
match self.body {
None => None,
Some(ref body) => Some(body.as_slice()),
}
}
pub fn body_as_string(&self) -> CabotResult<Option<String>> {
let body = match self.body {
None => return Ok(None),
Some(ref body) => {
let mut body_vec: Vec<u8> = Vec::new();
body_vec.extend_from_slice(body);
String::from_utf8(body_vec)?
}
};
Ok(Some(body))
}
pub fn http_version(&self) -> &str {
self.http_version.as_str()
}
pub fn host(&self) -> &str {
self.host.as_str()
}
pub fn port(&self) -> u16 {
self.port
}
pub fn authority(&self) -> &str {
self.authority.as_str()
}
pub fn scheme(&self) -> &str {
self.scheme.as_str()
}
pub fn request_uri(&self) -> &str {
self.request_uri.as_str()
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut resp = Vec::with_capacity(
1024 + match self.body() {
Some(bytes) => bytes.len(),
None => 0,
},
);
resp.extend_from_slice(
format!(
"{} {} {}\r\n",
self.http_method(),
self.request_uri(),
self.http_version()
)
.as_bytes(),
);
for header in self.headers.as_slice() {
resp.extend_from_slice(format!("{}\r\n", header).as_bytes());
}
if self.is_domain {
resp.extend_from_slice(format!("Host: {}\r\n", self.host()).as_bytes());
}
resp.extend_from_slice(b"Connection: close\r\n");
if let Some(payload) = self.body() {
resp.extend_from_slice(format!("Content-Length: {}\r\n\r\n", payload.len()).as_bytes());
resp.extend_from_slice(payload);
} else {
resp.extend_from_slice(b"\r\n");
}
resp
}
pub fn to_string(&self) -> String {
let req = self.to_bytes();
String::from_utf8(req).unwrap()
}
}
pub struct RequestBuilder {
http_method: String,
user_agent: String,
url: Result<Url, url::ParseError>,
http_version: String,
headers: Vec<String>,
body: Option<Vec<u8>>,
}
impl RequestBuilder {
pub fn new(url: &str) -> Self {
let url = url.parse::<Url>();
RequestBuilder {
http_method: "GET".to_owned(),
url,
user_agent: constants::user_agent(),
http_version: "HTTP/1.1".to_owned(),
headers: Vec::new(),
body: None,
}
}
pub fn set_url(mut self, url: &str) -> Self {
self.url = url.parse::<Url>();
self
}
pub fn set_http_method(mut self, http_method: &str) -> Self {
self.http_method = http_method.to_owned();
self
}
pub fn set_http_version(mut self, http_version: &str) -> Self {
self.http_version = http_version.to_owned();
self
}
pub fn add_header(mut self, header: &str) -> Self {
self.headers.push(header.to_owned());
self
}
pub fn add_headers(mut self, headers: &[&str]) -> Self {
for header in headers {
self.headers.push(header.to_string());
}
self
}
pub fn set_user_agent(mut self, user_agent: &str) -> Self {
self.user_agent = user_agent.to_owned();
self
}
pub fn set_body(mut self, buf: &[u8]) -> Self {
let mut body = Vec::with_capacity(buf.len());
body.extend_from_slice(buf);
self.body = Some(body);
self
}
pub fn set_body_as_str(self, body: &str) -> Self {
self.set_body(body.as_bytes())
}
pub fn build(&self) -> CabotResult<Request> {
let url = self.url.as_ref().map_err(|err| *err)?;
let host = url.host_str().ok_or(CabotError::OpaqueUrlError(
"Unable to find host".to_string(),
))?;
let port = url
.port_or_known_default()
.ok_or(CabotError::OpaqueUrlError(
"Unable to determine a port".to_string(),
))?;
let query = url.query();
let mut request_uri = url.path().to_owned();
if let Some(querystring) = query {
request_uri.push_str("?");
request_uri.push_str(querystring);
}
let is_domain = url.domain().is_some();
let mut headers = self.headers.clone();
headers.push(format!("User-Agent: {}", self.user_agent));
Ok(Request::new(
host.to_owned(),
port,
format!("{}:{}", host, port),
is_domain,
url.scheme().to_owned(),
self.http_method.clone(),
request_uri,
self.http_version.clone(),
headers,
match self.body {
Some(ref body) => Some(body.clone()),
None => None,
},
))
}
}
#[cfg(test)]
mod tests {
use super::super::constants;
use super::*;
#[test]
fn test_get_request_to_string() {
let request = Request::new(
"127.0.0.1".to_owned(),
80,
"127.0.0.1:80".to_owned(),
false,
"http".to_owned(),
"GET".to_owned(),
"/path?query".to_owned(),
"HTTP/1.1".to_owned(),
Vec::new(),
None,
);
let attempt = "GET /path?query HTTP/1.1\r\nConnection: close\r\n\r\n";
assert_eq!(request.to_string(), attempt);
}
#[test]
fn test_get_request_with_host_to_string() {
let request = Request::new(
"localhost".to_owned(),
80,
"localhost:80".to_owned(),
true,
"http".to_owned(),
"GET".to_owned(),
"/path?query".to_owned(),
"HTTP/1.1".to_owned(),
Vec::new(),
None,
);
let attempt = "GET /path?query HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n";
assert_eq!(request.to_string(), attempt);
}
#[test]
fn test_get_request_with_headers_to_string() {
let request = Request::new(
"localhost".to_owned(),
80,
"localhost:80".to_owned(),
true,
"http".to_owned(),
"GET".to_owned(),
"/path?query".to_owned(),
"HTTP/1.1".to_owned(),
vec![
"Accept-Language: fr".to_owned(),
"Accept-Encoding: gzip".to_owned(),
],
None,
);
let attempt = "GET /path?query HTTP/1.1\r\nAccept-Language: fr\r\nAccept-Encoding: \
gzip\r\nHost: localhost\r\nConnection: close\r\n\r\n";
assert_eq!(request.to_string(), attempt);
}
#[test]
fn test_post_request_with_headers_to_string() {
let body: Vec<u8> = vec![123, 125];
let request = Request::new(
"localhost".to_owned(),
80,
"localhost:80".to_owned(),
true,
"http".to_owned(),
"POST".to_owned(),
"/".to_owned(),
"HTTP/1.1".to_owned(),
vec![
"Accept-Language: fr".to_owned(),
"Content-Type: application/json".to_owned(),
],
Some(body),
);
let attempt = "POST / HTTP/1.1\r\nAccept-Language: fr\r\nContent-Type: \
application/json\r\nHost: localhost\r\nConnection: \
close\r\nContent-Length: 2\r\n\r\n{}";
assert_eq!(request.to_string(), attempt);
}
#[test]
fn test_request_builder_simple() {
let request = RequestBuilder::new("http://localhost/").build().unwrap();
assert_eq!(request.host(), "localhost".to_string());
assert_eq!(request.scheme(), "http".to_string());
assert_eq!(request.body, None);
assert_eq!(request.http_method(), "GET".to_string());
assert_eq!(request.http_version(), "HTTP/1.1".to_string());
let headers: Vec<String> = vec![format!("User-Agent: {}", constants::user_agent())];
assert_eq!(request.headers, headers);
}
#[test]
fn test_request_builder_complete() {
let builder = RequestBuilder::new("http://localhost/")
.set_http_method("POST")
.set_http_version("HTTP/1.0")
.set_user_agent("anonymized")
.add_header("Content-Type: application/json")
.add_headers(&["Accept-Encoding: deflate", "Accept-Language: fr"])
.set_body_as_str("{}");
let body: &[u8] = &[123, 125];
let request = builder.build().unwrap();
assert_eq!(request.host(), "localhost".to_string());
assert_eq!(request.body(), Some(body));
assert_eq!(request.body_as_string().unwrap().unwrap(), "{}".to_string());
assert_eq!(request.scheme(), "http".to_string());
assert_eq!(request.http_method(), "POST".to_string());
assert_eq!(request.request_uri(), "/");
assert_eq!(request.http_version(), "HTTP/1.0".to_string());
assert_eq!(
request.headers,
vec![
"Content-Type: application/json".to_string(),
"Accept-Encoding: deflate".to_string(),
"Accept-Language: fr".to_string(),
"User-Agent: anonymized".to_string(),
]
);
let builder = builder.set_url("http://[::1]/path");
let request = builder.build().unwrap();
assert_eq!(request.host(), "[::1]".to_string());
assert_eq!(request.request_uri(), "/path");
assert_eq!(request.body(), Some(body));
assert_eq!(request.body_as_string().unwrap().unwrap(), "{}".to_string());
assert_eq!(request.scheme(), "http".to_string());
assert_eq!(request.http_method(), "POST".to_string());
assert_eq!(request.http_version(), "HTTP/1.0".to_string());
assert_eq!(
request.headers,
vec![
"Content-Type: application/json".to_string(),
"Accept-Encoding: deflate".to_string(),
"Accept-Language: fr".to_string(),
"User-Agent: anonymized".to_string(),
]
);
let builder = builder.set_url("not_an_url");
let err = builder.build();
assert!(err.is_err());
}
}