mod ext_certs;
pub mod stream_response;
pub const DANGER_ACCEPT_INVALID_HOSTNAMES: &str = "danger_accept_invalid_hostnames";
pub const DANGER_ACCEPT_INVALID_CERTS: &str = "danger_accept_invalid_certs";
use std::{
collections::HashMap, str::FromStr, sync::Arc
};
use bt_any_error::any_err::AnyErr;
use bt_logger::{get_error, log_error, log_verbose, log_warning};
use ext_certs::get_local_certificates;
use reqwest::{
Client, Method, Response, StatusCode, Url, cookie::Jar, header::{self, HeaderMap, HeaderName, HeaderValue}
};
use stream_response::HttpStreamResponse;
pub struct HttpClient {
client: Client,
headers: HeaderMap,
}
#[derive(Clone, Debug)]
pub struct HttpResponse {
pub status_code: u16,
pub header: HashMap<String, String>,
pub body: String,
pub remote_address: String
}
#[derive(Debug)]
pub enum ContentType {
JSON,
TEXT,
}
impl HttpClient {
pub fn new(use_hickory_dns: bool, use_cookies: bool, danger_accept_invalid: Option<Vec<(String,bool)>>) -> Self {
let tls_conn = get_local_certificates(danger_accept_invalid);
let mut cb = Client::builder();
if use_cookies {
let cookie_store = Arc::new(Jar::default());
if let Some (reqwest_tc) = tls_conn{
cb = cb
.use_native_tls()
.use_preconfigured_tls(reqwest_tc);
}
cb = cb.cookie_provider(cookie_store.clone())
} else {
if let Some (reqwest_tc) = tls_conn{
cb = cb
.use_native_tls()
.use_preconfigured_tls(reqwest_tc);
}
cb = cb.cookie_store(false)
};
let c = cb
.connection_verbose(true)
.hickory_dns(use_hickory_dns)
.build()
.unwrap();
let mut h = HeaderMap::new();
h.insert(
header::USER_AGENT,
HeaderValue::from_static("Mozilla/5.0 (compatible; BachueTech/1.0)"),
);
Self {
client: c,
headers: h,
}
}
pub fn set_header(&mut self, header_name: &str, header_value: &str) {
self.headers.insert(
HeaderName::from_str(header_name).unwrap(),
HeaderValue::from_str(header_value).unwrap(),
);
}
pub fn get_default_headers(&self) -> HashMap<String, String> {
convert_headers(&self.headers)
}
fn get_extra_headers(&self, extra_headers: Option<HashMap<String, String>>) -> HeaderMap {
let mut local_headers = self.headers.clone();
if let Some(new_headers) = extra_headers {
for (key, value) in new_headers {
local_headers.insert(
HeaderName::from_str(&key).unwrap(),
HeaderValue::from_str(&value).unwrap(),
);
}
}
local_headers
}
pub async fn get( &self, url: &str, extra_headers: Option<HashMap<String, String>>, ) -> Result<HttpResponse, AnyErr> {
let local_headers = self.get_extra_headers(extra_headers);
let resp = self.client.get(url).headers(local_headers).send().await?;
Ok(Self::extract_response(resp, url, "GET").await)
}
pub async fn post( &self, url: &str, extra_headers: Option<HashMap<String, String>>, body_request: &str, content_type: ContentType, )
-> Result<HttpResponse, AnyErr> {
let mut local_headers = self.get_extra_headers(extra_headers); match content_type {
ContentType::JSON => {
local_headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_str("application/json")?,
);
}
ContentType::TEXT => {
local_headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_str("application/text")?,
);
}
}
let resp = self
.client
.post(url)
.headers(local_headers)
.body(body_request.to_string())
.send()
.await?;
return Ok(Self::extract_response(resp, url, "POST").await)
}
pub async fn post_stream( &self, url: &str, extra_headers: Option<HashMap<String, String>>, body_request: &str, content_type: ContentType, ) -> Result<HttpStreamResponse, AnyErr> {
let mut local_headers = self.get_extra_headers(extra_headers); match content_type {
ContentType::JSON => {
local_headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_str("application/json")?,
);
}
ContentType::TEXT => {
local_headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_str("application/text")?,
);
}
}
let resp = self
.client
.post(url)
.headers(local_headers)
.body(body_request.to_string())
.send()
.await?;
Ok(HttpStreamResponse::new(resp))
}
pub async fn request( &self, request_method: &str, url_with_ep_path: &str, extra_headers: Option<HashMap<String, String>>, body_params: Option<HashMap<String, String>>,
query_params: Option<HashMap<String, String>>, content_type: ContentType, ) -> Result<HttpResponse, AnyErr> {
let method = match request_method.to_uppercase().as_str() {
"GET" => Method::GET,
"POST" => Method::POST,
"PUT" => Method::PUT,
"DELETE" => Method::DELETE,
"PATCH" => Method::PATCH,
_ => return Err(get_error!("request", "Unsupported HTTP method: {}", &request_method).into()),
};
let mut url = url_with_ep_path.to_string();
let mut qry_params: HashMap<String, String>;
if let Some(path_params) = query_params {
qry_params = path_params.clone();
for path_param in path_params {
if url.contains(&format!("{{{}}}", &path_param.0)) {
url = url.replace(&format!("{{{}}}", &path_param.0), &path_param.1);
qry_params.remove(&path_param.0); } else {
log_verbose!("","Path parameter '{:?}' not provided. Parameter will be used as Query parameter", &path_param.0);
}
}
}else{
qry_params = HashMap::new();
}
let mut local_headers = self.get_extra_headers(extra_headers);
match content_type {
ContentType::JSON => {
local_headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_str("application/json")?,
);
}
ContentType::TEXT => {
local_headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_str("application/text")?,
);
}
}
let request =
if method == Method::GET{
let url_with_params = if !qry_params.is_empty() {
Url::parse_with_params(&url, &qry_params)?
}else{
Url::parse(&url)?
};
self.client.get(url_with_params).headers(local_headers)
}else{
let mut req = self.client.request(method.clone(), &url).headers(local_headers);
if let Some(b_params) = body_params{
match content_type {
ContentType::JSON => req = req.json(&b_params),
_ => {let body_data = b_params
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<String>>()
.join("&");
req = req.body(body_data)
}
}
}
req
};
let resp = request.send().await?;
Ok(Self::extract_response(resp, &url, request_method.to_uppercase().as_str()).await)
}
async fn extract_response(mut resp: Response, url: &str, method: &str) -> HttpResponse {
let ra = match resp.remote_addr() {
Some(ip) => ip.ip().to_string(),
None => {
log_warning!("", "Remote Address not found in Response. Using default 0.0.0.0");
"0.0.0.0".to_owned()
},
};
if resp.status().is_client_error() || resp.status().is_server_error() || resp.status().as_u16() >= 600 {
log_error!( "", "ERROR: Failed to get response from {}: {} Status Code: {}", method, url, resp.status() );
HttpResponse {
status_code: resp.status().as_u16(),
header: convert_headers(resp.headers()),
body: format!( "ERROR: Failed to get response from {}:{} -Error: {}", method, url, resp.status().canonical_reason().unwrap_or("UNKNOWN ERROR!") ),
remote_address: ra
}
} else {
let mut full_body = String::new();
let mut error_count = 0;
let rstatus = resp.status().as_u16();
let rheader = convert_headers(resp.headers());
if resp.status().is_success() {
let mut read_resp: bool = true;
while read_resp {
match resp.chunk().await {
Ok(r) => {
match r{
Some(chunk) => full_body.push_str(&String::from_utf8_lossy(&chunk)),
None => read_resp = false,
}
},
Err(e) => {
if error_count > 3{
log_error!("","Too many errors (>3 times) reading answer body. Stop Executing and return what was collected. Error {}",e);
return HttpResponse {
status_code: resp.status().as_u16(),
header: convert_headers(resp.headers()),
body: match resp.text().await{
Ok(b) => b,
Err(e) => {
log_error!("","ERROR: Failed to get payload from {}:{}. Error: {}",method,url,e);
full_body
},
},
remote_address: ra,
};
}
error_count += 1;
log_error!("","Error reading answer body (error count={}). Error {}",error_count,e);
},
}
}
}else{
full_body = match resp.text().await{
Ok(b) => b,
Err(e) => {
log_error!("","ERROR: Failed to get payload when status = {} from {}:{}. Error: {}",rstatus, method,url,e);
format!("ERROR: Failed to get payload when status = {} from {}",rstatus, method)
},
};
}
HttpResponse {
status_code: rstatus, header: rheader, body: full_body,
remote_address: ra,
}
}
}
}
fn convert_headers(headers: &HeaderMap) -> HashMap<String, String> {
headers
.iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or_default().to_string()))
.collect()
}
impl HttpResponse {
pub fn is_error(&self) -> bool {
let sc = StatusCode::from_u16(self.status_code).unwrap_or(StatusCode::FORBIDDEN);
sc.is_client_error() || sc.is_server_error()
}
}