use crate::error::Error;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use std::collections::HashMap;
use std::fs;
use std::fs::File;
use std::path::Path;
use urlencoding::{decode, encode};
#[derive(Clone, Debug)]
pub struct HttpBody {
is_form_post: bool,
params: HashMap<String, String>,
raw: Vec<u8>,
boundary: String,
files: HashMap<String, String>,
}
impl HttpBody {
pub fn new(params: &HashMap<String, String>, raw: &[u8]) -> Self {
let boundary: String = thread_rng()
.sample_iter(&Alphanumeric)
.take(30)
.map(|c| c as char)
.collect();
Self {
is_form_post: params.keys().len() > 0 || raw.len() > 0,
params: params.clone(),
raw: raw.clone().to_vec(),
boundary,
files: HashMap::new(),
}
}
pub fn empty() -> Self {
Self::new(&HashMap::new(), &Vec::new())
}
pub fn from_string(data: &str) -> Self {
let mut params: HashMap<String, String> = HashMap::new();
for pair in data.split('&') {
if let Some(index) = pair.find('=') {
params.insert(
pair[..index].to_string(),
decode(pair[index + 1..].trim()).unwrap().to_string(),
);
}
}
Self::new(¶ms, &Vec::new())
}
pub fn from_map(params: &HashMap<&str, &str>) -> Self {
let formatted_params = params
.iter()
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect();
Self::new(&formatted_params, &Vec::new())
}
pub fn from_raw(data: &[u8]) -> Self {
Self::new(&HashMap::new(), data)
}
pub fn from_raw_str(data: &str) -> Self {
Self::new(&HashMap::new(), data.as_bytes())
}
pub fn set_param(&mut self, key: &str, value: &str) {
*self
.params
.entry(key.to_string())
.or_insert(value.to_string()) = value.to_string();
self.is_form_post = true;
}
pub fn upload_file(&mut self, param_name: &str, file_path: &str) -> Result<(), Error> {
if !Path::new(&file_path).exists() {
return Err(Error::FileNotExists(file_path.to_string()));
}
*self
.files
.entry(param_name.to_string())
.or_insert(file_path.to_string()) = file_path.to_string();
Ok(())
}
pub fn format(&self) -> Vec<u8> {
if !self.files.is_empty() {
return self.format_multipart();
} else if self.raw.len() > 0 {
return self.raw.clone();
} else if !self.is_form_post {
return Vec::new();
}
let body = self
.params
.iter()
.map(|(key, value)| format!("{}={}", key, encode(value)))
.collect::<Vec<String>>()
.join("&");
body.as_bytes().to_vec()
}
fn format_multipart(&self) -> Vec<u8> {
let mut body: Vec<u8> = Vec::new();
for (key, value) in self.params.iter() {
let section = format!(
"--{}\r\nContent-Disposition: form-data; name=\"{}\"\r\n\r\n{}\r\n",
self.boundary, key, value
);
body.extend_from_slice(section.as_bytes());
}
for (key, filepath) in self.files.iter() {
let (filename, mime_type, contents) = self.get_file_info(filepath);
let section = format!("--{}\r\nContent-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n", self.boundary, key, filename, mime_type);
body.extend_from_slice(section.as_bytes());
body.extend_from_slice(&contents);
body.extend_from_slice("\r\n".as_bytes());
}
body.extend_from_slice(format!("--{}--\r\n", self.boundary).as_bytes());
body
}
fn get_file_info(&self, filepath: &String) -> (String, String, Vec<u8>) {
let pos = filepath
.rfind('/')
.or_else(|| filepath.rfind('\\'))
.unwrap();
let filename = filepath[pos + 1..].to_string();
let mime_guess = mime_guess::from_path(filepath);
let mime_type = if mime_guess.count() > 0 {
mime_guess.first().unwrap().to_string()
} else {
"application/octet-stream".to_string()
};
let _file = File::open(filepath).unwrap();
let content =
fs::read(filepath).unwrap_or_else(|_| panic!("Unable to read file at, {}", filepath));
(filename, mime_type, content)
}
pub fn is_form_post(&self) -> bool {
self.is_form_post
}
pub fn params(&self) -> HashMap<String, String> {
self.params.clone()
}
pub fn add_param(&mut self, key: &str, value: &str) {
*self.params.entry(key.to_string()).or_default() = value.to_string();
self.is_form_post = true;
}
pub fn get_raw(&self) -> Vec<u8> {
self.raw.clone()
}
pub fn boundary(&self) -> String {
self.boundary.clone()
}
pub fn files(&self) -> HashMap<String, String> {
self.files.clone()
}
}