1use json::{array, object, JsonValue};
2use log::debug;
3use std::collections::HashMap;
4use std::fmt::Debug;
5use std::{env, fs, str};
6use std::io::Write;
7use std::path::Path;
8use std::process::Command;
9use std::str::Lines;
10
11pub struct Client {
12 pub url: String,
13 debug: bool,
14 method: Method,
15 header: HashMap<String, String>,
16 body_data: JsonValue,
17 content_type: ContentType,
18}
19impl Default for Client {
20 fn default() -> Self {
21 Self::new()
22 }
23}
24
25impl Client {
26 pub fn new() -> Self {
27 Self {
28 url: "".to_string(),
29 debug: false,
30 method: Method::NONE,
31 header: Default::default(),
32 body_data: object! {},
33 content_type: ContentType::Text,
34 }
35 }
36 pub fn debug(&mut self) {
37 self.debug = true;
38 }
39 pub fn post(&mut self, url: &str) -> &mut Self {
40 self.url = url.to_string();
41 self.method = Method::POST;
42 self
43 }
44 pub fn get(&mut self, url: &str) -> &mut Self {
45 self.url = url.to_string();
46 self.method = Method::GET;
47 self
48 }
49 pub fn put(&mut self, url: &str) -> &mut Self {
50 self.url = url.to_string();
51 self.method = Method::PUT;
52 self
53 }
54 pub fn patch(&mut self, url: &str) -> &mut Self {
55 self.url = url.to_string();
56 self.method = Method::PATCH;
57 self
58 }
59 pub fn delete(&mut self, url: &str) -> &mut Self {
60 self.url = url.to_string();
61 self.method = Method::DELETE;
62 self
63 }
64 pub fn head(&mut self, url: &str) -> &mut Self {
65 self.url = url.to_string();
66 self.method = Method::HEAD;
67 self
68 }
69 pub fn trace(&mut self, url: &str) -> &mut Self {
70 self.url = url.to_string();
71 self.method = Method::TRACE;
72 self
73 }
74 pub fn header(&mut self, key: &str, value: &str) -> &mut Self {
75 self.header.insert(key.to_string(), value.to_string());
76 self
77 }
78
79 pub fn query(&mut self, params: JsonValue) -> &mut Self {
80 let mut txt = vec![];
81 for (key, value) in params.entries() {
82 txt.push(format!("{}={}", key, value));
83 }
84 if self.url.contains('?') {
85 if !txt.is_empty() {
86 self.url = format!("{}&{}", self.url, txt.join("&"));
87 }
88 } else if !txt.is_empty() {
89 self.url = format!("{}?{}", self.url, txt.join("&"));
90 }
91 self
92 }
93
94
95 pub fn raw_json(&mut self, data: JsonValue) -> &mut Self {
97 self.header("Content-Type", ContentType::Json.str().as_str());
98 self.content_type = ContentType::Json;
99 self.body_data = data;
100 self
101 }
102 pub fn raw_stream(&mut self, data: Vec<u8>, content_type: &str) -> &mut Self {
103 self.header("Content-Type", content_type);
104 self.content_type = ContentType::Stream;
105 self.body_data = data.into();
106 self
107 }
108 pub fn form_data(&mut self, data: JsonValue) -> &mut Self {
110 self.header("Content-Type", ContentType::FormData.str().as_str());
111 self.content_type = ContentType::FormData;
112 self.body_data = data;
113 self
114 }
115 pub fn form_urlencoded(&mut self, data: JsonValue) -> &mut Self {
117 self.header("Content-Type", ContentType::FormUrlencoded.str().as_str());
118 self.content_type = ContentType::FormUrlencoded;
119 self.body_data = data;
120 self
121 }
122 pub fn send(&mut self) -> Result<Response, String> {
123 let mut output = Command::new("curl");
124 output.arg("-i");
125
126 output.arg("-X");
127 output.arg(self.method.to_str().to_uppercase());
128
129 output.arg(self.url.as_str());
130 if !self.header.is_empty() {
131 for (key, value) in self.header.iter() {
132 output.arg("-H");
133 output.arg(format!("{}: {}", key, value));
134 }
135 }
136
137 match self.content_type {
138 ContentType::FormData => {
139 for (_, value) in self.body_data.entries() {
140 output.arg("-F");
141 if value[2].is_empty() {
142 output.arg(format!("{}={}", value[0], value[1]));
143 } else {
144 output.arg(format!("{}={};{}", value[0], value[1], value[2]));
145 }
146 }
147 }
148 ContentType::FormUrlencoded => {
149 output.arg("-d");
150 let mut d = vec![];
151 for (key, value) in self.body_data.entries() {
152 d.push(format!("{}={}", key, value))
153 }
154 output.arg(d.join("&"));
155 }
156 ContentType::Json => {
157 output.arg("-d");
158 output.arg(format!("{}", self.body_data));
159 }
160 ContentType::Xml => {}
161 ContentType::Javascript => {}
162 ContentType::Text => {}
163 ContentType::Html => {}
164 ContentType::Other(_) => {}
165 ContentType::Stream => {
166 output.arg("-F");
167 output.arg(format!("file={}", self.body_data));
168 }
173 }
174
175 if self.debug {
176 println!("{}", self.url);
177 println!("{:?}", output);
178 }
179 let req = match output.output() {
180 Ok(e) => e,
181 Err(e) => {
182 return Err(e.to_string());
183 }
184 };
185
186 if req.status.success() {
187 let body = req.stdout.clone();
188 if self.debug {
189 let text = String::from_utf8_lossy(&req.stdout);
190 println!("响应内容:\n{}", text);
191 }
192 Ok(Response::new(self.debug, body)?)
193 } else {
194 let err = String::from_utf8_lossy(&req.stderr).to_string();
195 let txt = match err.find("ms:") {
196 None => err,
197 Some(e) => {
198 err[e + 3..].trim().to_string()
199 }
200 };
201 Err(txt)
202 }
203 }
204}
205
206
207#[derive(Debug)]
208pub struct Response {
209 debug: bool,
210 status: i32,
211 method: Method,
212 headers: JsonValue,
213 cookies: JsonValue,
214 body: Body,
216}
217impl Response {
218 const CRLF2: [u8; 4] = [13, 10, 13, 10];
219
220 fn new(debug: bool, body: Vec<u8>) -> Result<Self, String> {
221 let mut that = Self {
222 debug,
223 status: 0,
224 method: Method::NONE,
225 headers: object! {},
226 cookies: object! {},
227 body: Default::default(),
228 };
229
230 let (header, body) = match body.windows(Self::CRLF2.len()).position(|window| window == Self::CRLF2) {
231 None => (vec![], vec![]),
232 Some(index) => {
233 let header = body[..index].to_vec();
234 let body = Vec::from(&body[index + Self::CRLF2.len()..]);
235 (header, body)
236 }
237 };
238
239
240 let text = String::from_utf8_lossy(header.as_slice());
241 let lines = text.lines();
242
243 that.get_request_line(lines.clone().next().unwrap())?;
244 that.get_header(lines.clone())?;
246
247 that.body.set_content(body);
248 Ok(that)
249 }
250
251 fn get_request_line(&mut self, line: &str) -> Result<(), String> {
253 let lines = line.split_whitespace().collect::<Vec<&str>>();
254 if lines.len() < 2 {
255 return Err("请求行错误".to_string());
256 }
257 self.method = Method::from(lines[0]);
258 self.status = lines[1].parse::<i32>().unwrap();
259 Ok(())
260 }
261 fn get_header(&mut self, data: Lines) -> Result<(), String> {
263 let mut header = object! {};
264 let mut cookie = object! {};
265 let mut body = Body::default();
266
267 for text in data {
268 let (key, value) = match text.trim().find(":") {
269 None => continue,
270 Some(e) => {
271 let key = text[..e].trim().to_lowercase().clone();
272 let value = text[e + 1..].trim().to_string();
273 (key, value)
274 }
275 };
276 match key.as_str() {
277 "content-type" => match value {
278 _ if value.contains("multipart/form-data") => {
279 let boundarys = value.split("boundary=").collect::<Vec<&str>>();
280 body.boundary = boundarys[1..].join("");
281 body.content_type = ContentType::from("multipart/form-data");
282 let _ = header.insert(key.as_str(), "multipart/form-data");
283 }
284 _ => {
285 let value = match value.find(";") {
286 None => value,
287 Some(e) => value[..e].trim().to_string(),
288 };
289 body.content_type = ContentType::from(value.as_str());
290 let _ = header.insert(key.as_str(), body.content_type.str());
291 }
292 },
293 "content-length" => {
294 body.content_length = value.to_string().parse::<usize>().unwrap_or(0);
295 }
296 "cookie" => {
297 let _ = value.split(";").collect::<Vec<&str>>().iter().map(|&x| {
298 match x.find("=") {
299 None => {}
300 Some(index) => {
301 let key = x[..index].trim().to_string();
302 let val = x[index + 1..].trim().to_string();
303 let _ = cookie.insert(key.as_str(), val);
304 }
305 };
306 ""
307 }).collect::<Vec<&str>>();
308 }
309 _ => {
310 if self.debug {
311 debug!("header: {} = {}", key,value);
312 }
313 let _ = header.insert(key.as_str(), value);
314 }
315 };
316 }
317 self.headers = header.clone();
318 self.cookies = cookie.clone();
319 self.body = body.clone();
320 Ok(())
321 }
322 pub fn status(&self) -> i32 {
323 self.status
324 }
325 pub fn headers(&self) -> JsonValue {
326 self.headers.clone()
327 }
328 pub fn cookies(&self) -> JsonValue {
329 self.cookies.clone()
330 }
331 pub fn content_type(&self) -> String {
332 self.body.content_type.str().clone()
333 }
334 pub fn json(&self) -> Result<JsonValue, String> {
335 match json::parse(self.body.content.to_string().as_str()) {
336 Ok(e) => Ok(e),
337 Err(e) => Err(e.to_string())
338 }
339 }
340 pub fn body(&self) -> JsonValue {
341 self.body.content.clone()
342 }
343}
344
345#[derive(Clone, Debug)]
346pub enum Method {
347 GET,
348 POST,
349 OPTIONS,
350 PATCH,
351 HEAD,
352 DELETE,
353 TRACE,
354 PUT,
355 NONE,
356}
357
358impl Method {
359 pub fn to_str(&self) -> String {
360 match self {
361 Method::GET => "GET",
362 Method::POST => "POST",
363 Method::OPTIONS => "OPTIONS",
364 Method::PATCH => "PATCH",
365 Method::HEAD => "HEAD",
366 Method::DELETE => "DELETE",
367 Method::TRACE => "TRACE",
368 Method::PUT => "PUT",
369 Method::NONE => "NONE",
370 }.to_string()
371 }
372 pub fn from(name: &str) -> Self {
373 match name.to_lowercase().as_str() {
374 "post" => Self::POST,
375 "get" => Self::GET,
376 "head" => Self::HEAD,
377 "put" => Self::PUT,
378 "delete" => Self::DELETE,
379 "options" => Self::OPTIONS,
380 "patch" => Self::PATCH,
381 "trace" => Self::TRACE,
382 _ => Self::NONE,
383 }
384 }
385}
386#[derive(Clone, Debug)]
387pub enum Version {
388 Http09,
389 Http10,
390 Http11,
391 H2,
392 H3,
393 None,
394}
395
396impl Version {
397 pub fn str(&mut self) -> String {
398 match self {
399 Version::Http09 => "HTTP/0.9",
400 Version::Http10 => "HTTP/1.0",
401 Version::Http11 => "HTTP/1.1",
402 Version::H2 => "HTTP/2.0",
403 Version::H3 => "HTTP/3.0",
404 Version::None => "",
405 }.to_string()
406 }
407 pub fn from(name: &str) -> Version {
408 match name {
409 "HTTP/0.9" => Self::Http09,
410 "HTTP/1.0" => Self::Http10,
411 "HTTP/1.1" => Self::Http11,
412 "HTTP/2.0" => Self::H2,
413 "HTTP/3.0" => Self::H3,
414 _ => Self::None,
415 }
416 }
417 pub fn set_version(name: &str) -> Version {
418 match name {
419 "0.9" => Self::Http09,
420 "1.0" => Self::Http10,
421 "1.1" => Self::Http11,
422 "2.0" => Self::H2,
423 "3.0" => Self::H3,
424 _ => Self::None,
425 }
426 }
427}
428
429
430#[derive(Clone, Debug)]
431pub enum FormData {
432 Text(String, JsonValue, String),
436 File(String, String, String),
440 None,
441}
442
443
444#[derive(Debug, Clone)]
445pub struct Body {
446 pub content_type: ContentType,
447 pub boundary: String,
448 pub content_length: usize,
449 pub content: JsonValue,
450}
451impl Body {
452 pub fn decode(input: &str) -> Result<String, String> {
454 let mut decoded = String::new();
455 let bytes = input.as_bytes();
456 let mut i = 0;
457
458 while i < bytes.len() {
459 if bytes[i] == b'%' {
460 if i + 2 >= bytes.len() {
461 return Err("Incomplete percent-encoding".into());
462 }
463 let hex = &input[i + 1..i + 3];
464 match u8::from_str_radix(hex, 16) {
465 Ok(byte) => decoded.push(byte as char),
466 Err(_) => return Err(format!("Invalid percent-encoding: %{}", hex)),
467 }
468 i += 3;
469 } else if bytes[i] == b'+' {
470 decoded.push(' ');
471 i += 1;
472 } else {
473 decoded.push(bytes[i] as char);
474 i += 1;
475 }
476 }
477
478 Ok(decoded)
479 }
480 pub fn set_content(&mut self, data: Vec<u8>) {
481 match self.content_type.clone() {
482 ContentType::FormData => {
483 let mut fields = object! {};
484 let boundary_marker = format!("--{}", self.boundary);
485 let text = unsafe { String::from_utf8_unchecked(data) };
486 let parts = text.split(&boundary_marker).collect::<Vec<&str>>();
487 for part in parts {
488 let part = part.trim();
489 if part.is_empty() || part == "--" {
490 continue; }
492
493 let mut headers_and_body = part.splitn(2, "\r\n\r\n");
494 if let (Some(headers), Some(body)) = (headers_and_body.next(), headers_and_body.next())
495 {
496 let headers = headers.split("\r\n");
498
499 let mut field_name = "";
500 let mut filename = "";
501 let mut content_type = ContentType::Text;
502
503 for header in headers {
504 if header.to_lowercase().starts_with("content-disposition:") {
505 match header.find("filename=\"") {
506 None => {}
507 Some(filename_start) => {
508 let filename_len = filename_start + 10;
509 let filename_end = header[filename_len..].find('"').unwrap() + filename_len;
510 filename = &header[filename_len..filename_end];
511 }
512 }
513
514 match header.find("name=\"") {
515 None => {}
516 Some(name_start) => {
517 let name_start = name_start + 6;
518 let name_end = header[name_start..].find('"').unwrap() + name_start;
519 field_name = &header[name_start..name_end];
520 }
521 }
522 }
523 if header.to_lowercase().starts_with("content-type:") {
524 content_type = ContentType::from(
525 header.to_lowercase().trim_start_matches("content-type:").trim(),
526 );
527 }
528 }
529
530 if filename.is_empty() {
531 fields[field_name.to_string()] = JsonValue::from(body);
532 } else {
533 let mut temp_dir = env::temp_dir();
535 temp_dir.push(filename);
537 let mut temp_file = match fs::File::create(&temp_dir) {
539 Ok(e) => e,
540 Err(_) => continue,
541 };
542 if temp_file.write(body.as_bytes()).is_ok() {
543 if fields[field_name.to_string()].is_empty() {
544 fields[field_name.to_string()] = array![]
545 }
546
547 let extension = Path::new(filename).extension() .and_then(|ext| ext.to_str()); let suffix = extension.unwrap_or("txt");
551
552 fields[field_name.to_string()].push(object! {
553 name:filename,
555 suffix:suffix,
556 size:body.len(),
557 "type":content_type.str(),
558 file:temp_dir.to_str()
559 }).unwrap();
560 };
561 }
562 }
563 }
564 self.content = fields;
565 }
566 ContentType::FormUrlencoded => {
567 let text = unsafe { String::from_utf8_unchecked(data) };
568 let params = text.split("&").collect::<Vec<&str>>();
569 let mut list = object! {};
570 for param in params.iter() {
571 let t = param.split("=").collect::<Vec<&str>>().iter().map(|&x| Body::decode(x).unwrap_or(x.to_string())).collect::<Vec<String>>();
572 list[t[0].to_string()] = t[1].clone().into();
573 }
574 self.content = list;
575 }
576 ContentType::Json => {
577 let text = unsafe { String::from_utf8_unchecked(data) };
578 self.content = json::parse(text.as_str()).unwrap_or(object! {});
579 }
580 ContentType::Xml => {
581 let text = unsafe { String::from_utf8_unchecked(data) };
582 self.content = text.into();
583 }
584 ContentType::Html | ContentType::Text | ContentType::Javascript => {
585 let text = unsafe { String::from_utf8_unchecked(data) };
586 self.content = text.into();
587 }
588 ContentType::Other(name) => match name.as_str() {
589 "application/pdf" => {
590 let text = unsafe { String::from_utf8_unchecked(data) };
591 self.content = text.into();
592 }
593 _ => {
594 let text = unsafe { String::from_utf8_unchecked(data) };
595 self.content = text.into();
596 }
597 },
598 ContentType::Stream => {}
599 }
600 }
601}
602
603impl Default for Body {
604 fn default() -> Self {
605 Self {
606 content_type: ContentType::Other("text/plain".to_string()),
607 boundary: "".to_string(),
608 content_length: 0,
609 content: object! {},
610 }
611 }
612}
613
614#[derive(Debug, Clone)]
616pub enum ContentType {
617 FormData,
618 FormUrlencoded,
619 Json,
620 Xml,
621 Javascript,
622 Text,
623 Html,
624 Stream,
625 Other(String),
626}
627impl ContentType {
628 pub fn from(name: &str) -> Self {
629 match name {
630 "multipart/form-data" => Self::FormData,
631 "application/x-www-form-urlencoded" => Self::FormUrlencoded,
632 "application/json" => Self::Json,
633 "application/xml" | "text/xml" => Self::Xml,
634 "application/javascript" => Self::Javascript,
635 "text/html" => Self::Html,
636 "text/plain" => Self::Text,
637 "application/octet-stream" => Self::Stream,
638 _ => Self::Other(name.to_string()),
639 }
640 }
641 pub fn str(&self) -> String {
642 match self {
643 Self::FormData => "multipart/form-data",
644 Self::FormUrlencoded => "application/x-www-form-urlencoded",
645 Self::Json => "application/json",
646 Self::Xml => "application/xml",
647 Self::Javascript => "application/javascript",
648 Self::Text => "text/plain",
649 Self::Html => "text/html",
650 Self::Other(name) => name,
651 Self::Stream => "application/octet-stream",
652 }.to_string()
653 }
654}