use json::{array, object, JsonValue};
use log::debug;
use std::collections::HashMap;
use std::fmt::Debug;
use std::{env, fs, str};
use std::io::Write;
use std::path::Path;
use std::process::Command;
use std::str::Lines;
pub struct Client {
pub url: String,
debug: bool,
method: Method,
header: HashMap<String, String>,
body_data: JsonValue,
content_type: ContentType,
}
impl Default for Client {
fn default() -> Self {
Self::new()
}
}
impl Client {
pub fn new() -> Self {
Self {
url: "".to_string(),
debug: false,
method: Method::NONE,
header: Default::default(),
body_data: object! {},
content_type: ContentType::Text,
}
}
pub fn debug(&mut self) {
self.debug = true;
}
pub fn post(&mut self, url: &str) -> &mut Self {
self.url = url.to_string();
self.method = Method::POST;
self
}
pub fn get(&mut self, url: &str) -> &mut Self {
self.url = url.to_string();
self.method = Method::GET;
self
}
pub fn put(&mut self, url: &str) -> &mut Self {
self.url = url.to_string();
self.method = Method::PUT;
self
}
pub fn patch(&mut self, url: &str) -> &mut Self {
self.url = url.to_string();
self.method = Method::PATCH;
self
}
pub fn delete(&mut self, url: &str) -> &mut Self {
self.url = url.to_string();
self.method = Method::DELETE;
self
}
pub fn head(&mut self, url: &str) -> &mut Self {
self.url = url.to_string();
self.method = Method::HEAD;
self
}
pub fn trace(&mut self, url: &str) -> &mut Self {
self.url = url.to_string();
self.method = Method::TRACE;
self
}
pub fn header(&mut self, key: &str, value: &str) -> &mut Self {
self.header.insert(key.to_string(), value.to_string());
self
}
pub fn query(&mut self, params: JsonValue) -> &mut Self {
let mut txt = vec![];
for (key, value) in params.entries() {
txt.push(format!("{}={}", key, value));
}
if self.url.contains('?') {
if !txt.is_empty() {
self.url = format!("{}&{}", self.url, txt.join("&"));
}
} else if !txt.is_empty() {
self.url = format!("{}?{}", self.url, txt.join("&"));
}
self
}
pub fn raw_json(&mut self, data: JsonValue) -> &mut Self {
self.header("Content-Type", ContentType::Json.str().as_str());
self.content_type = ContentType::Json;
self.body_data = data;
self
}
pub fn raw_stream(&mut self, data: Vec<u8>, content_type: &str) -> &mut Self {
self.header("Content-Type", content_type);
self.content_type = ContentType::Stream;
self.body_data = data.into();
self
}
pub fn form_data(&mut self, data: JsonValue) -> &mut Self {
self.header("Content-Type", ContentType::FormData.str().as_str());
self.content_type = ContentType::FormData;
self.body_data = data;
self
}
pub fn form_urlencoded(&mut self, data: JsonValue) -> &mut Self {
self.header("Content-Type", ContentType::FormUrlencoded.str().as_str());
self.content_type = ContentType::FormUrlencoded;
self.body_data = data;
self
}
pub fn send(&mut self) -> Result<Response, String> {
let mut output = Command::new("curl");
output.arg("-i");
output.arg("-X");
output.arg(self.method.to_str().to_uppercase());
output.arg(self.url.as_str());
if !self.header.is_empty() {
for (key, value) in self.header.iter() {
output.arg("-H");
output.arg(format!("{}: {}", key, value));
}
}
match self.content_type {
ContentType::FormData => {
for (_, value) in self.body_data.entries() {
output.arg("-F");
if value[2].is_empty() {
output.arg(format!("{}={}", value[0], value[1]));
} else {
output.arg(format!("{}={};{}", value[0], value[1], value[2]));
}
}
}
ContentType::FormUrlencoded => {
output.arg("-d");
let mut d = vec![];
for (key, value) in self.body_data.entries() {
d.push(format!("{}={}", key, value))
}
output.arg(d.join("&"));
}
ContentType::Json => {
output.arg("-d");
output.arg(format!("{}", self.body_data));
}
ContentType::Xml => {}
ContentType::Javascript => {}
ContentType::Text => {}
ContentType::Html => {}
ContentType::Other(_) => {}
ContentType::Stream => {
output.arg("-F");
output.arg(format!("file={}", self.body_data));
}
}
if self.debug {
println!("{}", self.url);
println!("{:?}", output);
}
let req = match output.output() {
Ok(e) => e,
Err(e) => {
return Err(e.to_string());
}
};
if req.status.success() {
let body = req.stdout.clone();
if self.debug {
let text = String::from_utf8_lossy(&req.stdout);
println!("响应内容:\n{}", text);
}
Ok(Response::new(self.debug, body)?)
} else {
let err = String::from_utf8_lossy(&req.stderr).to_string();
let txt = match err.find("ms:") {
None => err,
Some(e) => {
err[e + 3..].trim().to_string()
}
};
Err(txt)
}
}
}
#[derive(Debug)]
pub struct Response {
debug: bool,
status: i32,
method: Method,
headers: JsonValue,
cookies: JsonValue,
body: Body,
}
impl Response {
const CRLF2: [u8; 4] = [13, 10, 13, 10];
fn new(debug: bool, body: Vec<u8>) -> Result<Self, String> {
let mut that = Self {
debug,
status: 0,
method: Method::NONE,
headers: object! {},
cookies: object! {},
body: Default::default(),
};
let (header, body) = match body.windows(Self::CRLF2.len()).position(|window| window == Self::CRLF2) {
None => (vec![], vec![]),
Some(index) => {
let header = body[..index].to_vec();
let body = Vec::from(&body[index + Self::CRLF2.len()..]);
(header, body)
}
};
let text = String::from_utf8_lossy(header.as_slice());
let lines = text.lines();
that.get_request_line(lines.clone().next().unwrap())?;
that.get_header(lines.clone())?;
that.body.set_content(body);
Ok(that)
}
fn get_request_line(&mut self, line: &str) -> Result<(), String> {
let lines = line.split_whitespace().collect::<Vec<&str>>();
if lines.len() < 2 {
return Err("请求行错误".to_string());
}
self.method = Method::from(lines[0]);
self.status = lines[1].parse::<i32>().unwrap();
Ok(())
}
fn get_header(&mut self, data: Lines) -> Result<(), String> {
let mut header = object! {};
let mut cookie = object! {};
let mut body = Body::default();
for text in data {
let (key, value) = match text.trim().find(":") {
None => continue,
Some(e) => {
let key = text[..e].trim().to_lowercase().clone();
let value = text[e + 1..].trim().to_string();
(key, value)
}
};
match key.as_str() {
"content-type" => match value {
_ if value.contains("multipart/form-data") => {
let boundarys = value.split("boundary=").collect::<Vec<&str>>();
body.boundary = boundarys[1..].join("");
body.content_type = ContentType::from("multipart/form-data");
let _ = header.insert(key.as_str(), "multipart/form-data");
}
_ => {
let value = match value.find(";") {
None => value,
Some(e) => value[..e].trim().to_string(),
};
body.content_type = ContentType::from(value.as_str());
let _ = header.insert(key.as_str(), body.content_type.str());
}
},
"content-length" => {
body.content_length = value.to_string().parse::<usize>().unwrap_or(0);
}
"cookie" => {
let _ = value.split(";").collect::<Vec<&str>>().iter().map(|&x| {
match x.find("=") {
None => {}
Some(index) => {
let key = x[..index].trim().to_string();
let val = x[index + 1..].trim().to_string();
let _ = cookie.insert(key.as_str(), val);
}
};
""
}).collect::<Vec<&str>>();
}
_ => {
if self.debug {
debug!("header: {} = {}", key,value);
}
let _ = header.insert(key.as_str(), value);
}
};
}
self.headers = header.clone();
self.cookies = cookie.clone();
self.body = body.clone();
Ok(())
}
pub fn status(&self) -> i32 {
self.status
}
pub fn headers(&self) -> JsonValue {
self.headers.clone()
}
pub fn cookies(&self) -> JsonValue {
self.cookies.clone()
}
pub fn content_type(&self) -> String {
self.body.content_type.str().clone()
}
pub fn json(&self) -> Result<JsonValue, String> {
match json::parse(self.body.content.to_string().as_str()) {
Ok(e) => Ok(e),
Err(e) => Err(e.to_string())
}
}
pub fn body(&self) -> JsonValue {
self.body.content.clone()
}
}
#[derive(Clone, Debug)]
pub enum Method {
GET,
POST,
OPTIONS,
PATCH,
HEAD,
DELETE,
TRACE,
PUT,
NONE,
}
impl Method {
pub fn to_str(&self) -> String {
match self {
Method::GET => "GET",
Method::POST => "POST",
Method::OPTIONS => "OPTIONS",
Method::PATCH => "PATCH",
Method::HEAD => "HEAD",
Method::DELETE => "DELETE",
Method::TRACE => "TRACE",
Method::PUT => "PUT",
Method::NONE => "NONE",
}.to_string()
}
pub fn from(name: &str) -> Self {
match name.to_lowercase().as_str() {
"post" => Self::POST,
"get" => Self::GET,
"head" => Self::HEAD,
"put" => Self::PUT,
"delete" => Self::DELETE,
"options" => Self::OPTIONS,
"patch" => Self::PATCH,
"trace" => Self::TRACE,
_ => Self::NONE,
}
}
}
#[derive(Clone, Debug)]
pub enum Version {
Http09,
Http10,
Http11,
H2,
H3,
None,
}
impl Version {
pub fn str(&mut self) -> String {
match self {
Version::Http09 => "HTTP/0.9",
Version::Http10 => "HTTP/1.0",
Version::Http11 => "HTTP/1.1",
Version::H2 => "HTTP/2.0",
Version::H3 => "HTTP/3.0",
Version::None => "",
}.to_string()
}
pub fn from(name: &str) -> Version {
match name {
"HTTP/0.9" => Self::Http09,
"HTTP/1.0" => Self::Http10,
"HTTP/1.1" => Self::Http11,
"HTTP/2.0" => Self::H2,
"HTTP/3.0" => Self::H3,
_ => Self::None,
}
}
pub fn set_version(name: &str) -> Version {
match name {
"0.9" => Self::Http09,
"1.0" => Self::Http10,
"1.1" => Self::Http11,
"2.0" => Self::H2,
"3.0" => Self::H3,
_ => Self::None,
}
}
}
#[derive(Clone, Debug)]
pub enum FormData {
Text(String, JsonValue, String),
File(String, String, String),
None,
}
#[derive(Debug, Clone)]
pub struct Body {
pub content_type: ContentType,
pub boundary: String,
pub content_length: usize,
pub content: JsonValue,
}
impl Body {
pub fn decode(input: &str) -> Result<String, String> {
let mut decoded = String::new();
let bytes = input.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' {
if i + 2 >= bytes.len() {
return Err("Incomplete percent-encoding".into());
}
let hex = &input[i + 1..i + 3];
match u8::from_str_radix(hex, 16) {
Ok(byte) => decoded.push(byte as char),
Err(_) => return Err(format!("Invalid percent-encoding: %{}", hex)),
}
i += 3;
} else if bytes[i] == b'+' {
decoded.push(' ');
i += 1;
} else {
decoded.push(bytes[i] as char);
i += 1;
}
}
Ok(decoded)
}
pub fn set_content(&mut self, data: Vec<u8>) {
match self.content_type.clone() {
ContentType::FormData => {
let mut fields = object! {};
let boundary_marker = format!("--{}", self.boundary);
let text = unsafe { String::from_utf8_unchecked(data) };
let parts = text.split(&boundary_marker).collect::<Vec<&str>>();
for part in parts {
let part = part.trim();
if part.is_empty() || part == "--" {
continue; }
let mut headers_and_body = part.splitn(2, "\r\n\r\n");
if let (Some(headers), Some(body)) = (headers_and_body.next(), headers_and_body.next())
{
let headers = headers.split("\r\n");
let mut field_name = "";
let mut filename = "";
let mut content_type = ContentType::Text;
for header in headers {
if header.to_lowercase().starts_with("content-disposition:") {
match header.find("filename=\"") {
None => {}
Some(filename_start) => {
let filename_len = filename_start + 10;
let filename_end = header[filename_len..].find('"').unwrap() + filename_len;
filename = &header[filename_len..filename_end];
}
}
match header.find("name=\"") {
None => {}
Some(name_start) => {
let name_start = name_start + 6;
let name_end = header[name_start..].find('"').unwrap() + name_start;
field_name = &header[name_start..name_end];
}
}
}
if header.to_lowercase().starts_with("content-type:") {
content_type = ContentType::from(
header.to_lowercase().trim_start_matches("content-type:").trim(),
);
}
}
if filename.is_empty() {
fields[field_name.to_string()] = JsonValue::from(body);
} else {
let mut temp_dir = env::temp_dir();
temp_dir.push(filename);
let mut temp_file = match fs::File::create(&temp_dir) {
Ok(e) => e,
Err(_) => continue,
};
if temp_file.write(body.as_bytes()).is_ok() {
if fields[field_name.to_string()].is_empty() {
fields[field_name.to_string()] = array![]
}
let extension = Path::new(filename).extension() .and_then(|ext| ext.to_str());
let suffix = extension.unwrap_or("txt");
fields[field_name.to_string()].push(object! {
name:filename,
suffix:suffix,
size:body.len(),
"type":content_type.str(),
file:temp_dir.to_str()
}).unwrap();
};
}
}
}
self.content = fields;
}
ContentType::FormUrlencoded => {
let text = unsafe { String::from_utf8_unchecked(data) };
let params = text.split("&").collect::<Vec<&str>>();
let mut list = object! {};
for param in params.iter() {
let t = param.split("=").collect::<Vec<&str>>().iter().map(|&x| Body::decode(x).unwrap_or(x.to_string())).collect::<Vec<String>>();
list[t[0].to_string()] = t[1].clone().into();
}
self.content = list;
}
ContentType::Json => {
let text = unsafe { String::from_utf8_unchecked(data) };
self.content = json::parse(text.as_str()).unwrap_or(object! {});
}
ContentType::Xml => {
let text = unsafe { String::from_utf8_unchecked(data) };
self.content = text.into();
}
ContentType::Html | ContentType::Text | ContentType::Javascript => {
let text = unsafe { String::from_utf8_unchecked(data) };
self.content = text.into();
}
ContentType::Other(name) => match name.as_str() {
"application/pdf" => {
let text = unsafe { String::from_utf8_unchecked(data) };
self.content = text.into();
}
_ => {
let text = unsafe { String::from_utf8_unchecked(data) };
self.content = text.into();
}
},
ContentType::Stream => {}
}
}
}
impl Default for Body {
fn default() -> Self {
Self {
content_type: ContentType::Other("text/plain".to_string()),
boundary: "".to_string(),
content_length: 0,
content: object! {},
}
}
}
#[derive(Debug, Clone)]
pub enum ContentType {
FormData,
FormUrlencoded,
Json,
Xml,
Javascript,
Text,
Html,
Stream,
Other(String),
}
impl ContentType {
pub fn from(name: &str) -> Self {
match name {
"multipart/form-data" => Self::FormData,
"application/x-www-form-urlencoded" => Self::FormUrlencoded,
"application/json" => Self::Json,
"application/xml" | "text/xml" => Self::Xml,
"application/javascript" => Self::Javascript,
"text/html" => Self::Html,
"text/plain" => Self::Text,
"application/octet-stream" => Self::Stream,
_ => Self::Other(name.to_string()),
}
}
pub fn str(&self) -> String {
match self {
Self::FormData => "multipart/form-data",
Self::FormUrlencoded => "application/x-www-form-urlencoded",
Self::Json => "application/json",
Self::Xml => "application/xml",
Self::Javascript => "application/javascript",
Self::Text => "text/plain",
Self::Html => "text/html",
Self::Other(name) => name,
Self::Stream => "application/octet-stream",
}.to_string()
}
}