1use crate::connection::Connection;
2use crate::http_url::{HttpUrl, Port};
3#[cfg(feature = "proxy")]
4use crate::proxy::Proxy;
5use crate::{Error, Response, ResponseLazy};
6use std::collections::HashMap;
7use std::fmt;
8use std::fmt::Write;
9use tokio::io::{AsyncReadExt, AsyncWriteExt};
10pub type URL = String;
12
13#[derive(Clone, PartialEq, Eq, Debug)]
15pub enum Method {
16 Get,
18 Head,
20 Post,
22 Put,
24 Delete,
26 Connect,
28 Options,
30 Trace,
32 Patch,
34 Custom(String),
37}
38
39impl fmt::Display for Method {
40 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
43 match *self {
44 Method::Get => write!(f, "GET"),
45 Method::Head => write!(f, "HEAD"),
46 Method::Post => write!(f, "POST"),
47 Method::Put => write!(f, "PUT"),
48 Method::Delete => write!(f, "DELETE"),
49 Method::Connect => write!(f, "CONNECT"),
50 Method::Options => write!(f, "OPTIONS"),
51 Method::Trace => write!(f, "TRACE"),
52 Method::Patch => write!(f, "PATCH"),
53 Method::Custom(ref s) => write!(f, "{}", s),
54 }
55 }
56}
57
58#[derive(Clone, PartialEq, Eq, Debug)]
74pub struct Request {
75 pub(crate) method: Method,
76 url: URL,
77 params: String,
78 pub headers: HashMap<String, String>,
79 body: Option<Vec<u8>>,
80 pub(crate) max_headers_size: Option<usize>,
81 pub(crate) max_status_line_len: Option<usize>,
82 max_redirects: usize,
83 pub redirect: bool,
84 #[cfg(feature = "proxy")]
85 pub(crate) proxy: Option<Proxy>,
86}
87
88impl Request {
89 pub fn new<T: Into<URL>>(method: Method, url: T) -> Request {
101 let mut headers = HashMap::new();
102 headers.insert("user-agent".to_owned(), "minreq/1.0".to_owned());
103 headers.insert("accept".to_owned(), "*/*".to_owned());
104 Request {
105 method,
106 url: url.into(),
107 params: String::new(),
108 headers,
109 body: None,
110 max_headers_size: None,
111 max_status_line_len: None,
112 max_redirects: 100,
113 redirect: false,
114 #[cfg(feature = "proxy")]
115 proxy: None,
116 }
117 }
118
119 pub fn with_headers<T, K, V>(mut self, headers: T) -> Request
122 where
123 T: IntoIterator<Item = (K, V)>,
124 K: Into<String>,
125 V: Into<String>,
126 {
127 let headers = headers.into_iter().map(|(k, v)| (k.into(), v.into()));
128 self.headers.extend(headers);
129 self
130 }
131
132 pub fn with_header<T: Into<String>, U: Into<String>>(mut self, key: T, value: U) -> Request {
135 self.headers.insert(key.into(), value.into());
136 self
137 }
138 pub fn with_redirect(mut self, follow: bool) -> Request {
139 self.redirect = follow;
140 self
141 }
142 pub fn with_body<T: Into<Vec<u8>>>(mut self, body: T) -> Request {
144 let body = body.into();
145 let body_length = body.len();
146 self.body = Some(body);
147 self.with_header("Content-Length", format!("{}", body_length))
148 }
149
150 pub fn with_param<T: Into<String>, U: Into<String>>(mut self, key: T, value: U) -> Request {
159 let key = key.into();
160 let key = urlencoding::encode(&key);
161 let value = value.into();
162 let value = urlencoding::encode(&value);
163
164 if !self.params.is_empty() {
165 self.params.push('&');
166 }
167 self.params.push_str(&key);
168 self.params.push('=');
169 self.params.push_str(&value);
170 self
171 }
172
173 pub fn with_max_redirects(mut self, max_redirects: usize) -> Request {
181 self.max_redirects = max_redirects;
182 self
183 }
184
185 pub fn with_max_headers_size<S: Into<Option<usize>>>(mut self, max_headers_size: S) -> Request {
201 self.max_headers_size = max_headers_size.into();
202 self
203 }
204
205 pub fn with_max_status_line_length<S: Into<Option<usize>>>(
220 mut self,
221 max_status_line_len: S,
222 ) -> Request {
223 self.max_status_line_len = max_status_line_len.into();
224 self
225 }
226
227 #[cfg(feature = "proxy")]
229 pub fn with_proxy(mut self, proxy: Proxy) -> Request {
230 self.proxy = Some(proxy);
231 self
232 }
233
234 pub async fn send(self) -> Result<Response, Error> {
245 let parsed_request = ParsedRequest::new(self)?;
246 let is_head = parsed_request.config.method == Method::Head;
247 if parsed_request.url.https {
248 #[cfg(feature = "https")]
249 let response = Connection::new(parsed_request).send_https().await?;
250 #[cfg(feature = "https")]
251 return Response::create(response, is_head).await;
252 #[cfg(not(feature = "https"))]
253 return Err(Error::HttpsFeatureNotEnabled);
254 } else {
255 let response = Connection::new(parsed_request).send().await?;
256 Response::create(response, is_head).await
257 }
258 }
259
260 pub async fn send_lazy(self) -> Result<ResponseLazy, Error> {
266 let parsed_request = ParsedRequest::new(self)?;
267 if parsed_request.url.https {
268 #[cfg(feature = "https")]
269 return Connection::new(parsed_request).send_https().await;
270 #[cfg(not(feature = "https"))]
271 return Err(Error::HttpsFeatureNotEnabled);
272 } else {
273 Connection::new(parsed_request).send().await
274 }
275 }
276 pub async fn send_with_stream<W, F>(
277 self,
278 stream: &mut W,
279 progress_fn: F,
280 ) -> Result<Response, Error>
281 where
282 F: Fn(u64, u64),
283 W: tokio::io::AsyncWrite + std::marker::Unpin,
284 {
285 let mut response = self.send_lazy().await?;
286 let default_content_lenth = String::from("0");
287 let content_lenth = response
288 .headers
289 .get("content-length")
290 .unwrap_or(&default_content_lenth);
291 let content_lenth = content_lenth.trim();
292 let content_lenth =
293 u64::from_str_radix(content_lenth, 10).map_err(|_| Error::MalformedContentLength)?;
294 let total_download =
295 copy_with_progress(&mut response.stream, stream, content_lenth, progress_fn).await?;
296 let mut response = Response::create(response, true).await?;
297 response.download_size = total_download;
298 Ok(response)
299 }
300}
301
302#[derive(Debug)]
303pub(crate) struct ParsedRequest {
304 pub(crate) url: HttpUrl,
305 pub(crate) redirects: Vec<HttpUrl>,
306 pub(crate) config: Request,
307}
308
309impl ParsedRequest {
310 #[allow(unused_mut)]
311 fn new(mut config: Request) -> Result<ParsedRequest, Error> {
312 let mut url = HttpUrl::parse(&config.url, None)?;
313
314 if !config.params.is_empty() {
315 if url.path_and_query.contains('?') {
316 url.path_and_query.push('&');
317 } else {
318 url.path_and_query.push('?');
319 }
320 url.path_and_query.push_str(&config.params);
321 }
322
323 #[cfg(feature = "proxy")]
324 if config.proxy.is_none() {
332 if url.https {
334 if let Ok(proxy) =
335 std::env::var("https_proxy").map_err(|_| std::env::var("HTTPS_PROXY"))
336 {
337 if let Ok(proxy) = Proxy::new(proxy) {
338 config.proxy = Some(proxy);
339 }
340 }
341 }
342 else if let Ok(proxy) = std::env::var("http_proxy") {
344 if let Ok(proxy) = Proxy::new(proxy) {
345 config.proxy = Some(proxy);
346 }
347 }
348 else if let Ok(proxy) =
350 std::env::var("all_proxy").map_err(|_| std::env::var("ALL_PROXY"))
351 {
352 if let Ok(proxy) = Proxy::new(proxy) {
353 config.proxy = Some(proxy);
354 }
355 }
356 }
357
358 Ok(ParsedRequest {
359 url,
360 redirects: Vec::new(),
361 config,
362 })
363 }
364
365 fn get_http_head(&self) -> String {
366 let mut http = String::with_capacity(32);
367
368 write!(
379 http,
380 "{} {} HTTP/1.1\r\nHost: {}",
381 self.config.method, self.url.path_and_query, self.url.host
382 )
383 .unwrap();
384 if let Port::Explicit(port) = self.url.port {
385 write!(http, ":{}", port).unwrap();
386 }
387 http += "\r\n";
388
389 for (k, v) in &self.config.headers {
391 write!(http, "{}: {}\r\n", k, v).unwrap();
392 }
393 if self.config.method == Method::Post
394 || self.config.method == Method::Put
395 || self.config.method == Method::Patch
396 {
397 let not_length = |key: &String| {
398 let key = key.to_lowercase();
399 key != "content-length" && key != "transfer-encoding"
400 };
401 if self.config.headers.keys().all(not_length) {
402 http += "Content-Length: 0\r\n";
411 }
412 }
413 http += "\r\n";
414 http
415 }
416
417 pub(crate) fn as_bytes(&self) -> Vec<u8> {
420 let mut head = self.get_http_head().into_bytes();
421 if let Some(body) = &self.config.body {
422 head.extend(body);
423 }
424 head
425 }
426
427 pub(crate) fn redirect_to(&mut self, url: &str) -> Result<(), Error> {
431 if url.contains("://") {
432 let mut url = HttpUrl::parse(url, Some(&self.url)).map_err(|_| {
433 Error::IoError(std::io::Error::new(
436 std::io::ErrorKind::Other,
437 "was redirected to an absolute url with an invalid protocol",
438 ))
439 })?;
440 log::trace!("Redirect to absolute url: {:?}", url);
441 std::mem::swap(&mut url, &mut self.url);
442 self.redirects.push(url);
443 } else {
444 log::trace!("Redirect to relatively url: {:?}", url);
447 let mut absolute_url = String::new();
448 self.url.write_base_url_to(&mut absolute_url).unwrap();
449 absolute_url.push_str(url);
450 let mut url = HttpUrl::parse(&absolute_url, Some(&self.url))?;
451 std::mem::swap(&mut url, &mut self.url);
452 self.redirects.push(url);
453 }
454
455 if self.redirects.len() > self.config.max_redirects {
456 Err(Error::TooManyRedirections)
457 } else if self
458 .redirects
459 .iter()
460 .any(|redirect_url| redirect_url == &self.url)
461 {
462 Err(Error::InfiniteRedirectionLoop)
463 } else {
464 Ok(())
465 }
466 }
467}
468
469pub fn get<T: Into<URL>>(url: T) -> Request {
472 Request::new(Method::Get, url)
473}
474
475pub fn head<T: Into<URL>>(url: T) -> Request {
478 Request::new(Method::Head, url)
479}
480
481pub fn post<T: Into<URL>>(url: T) -> Request {
484 Request::new(Method::Post, url)
485}
486
487pub fn put<T: Into<URL>>(url: T) -> Request {
490 Request::new(Method::Put, url)
491}
492
493pub fn delete<T: Into<URL>>(url: T) -> Request {
496 Request::new(Method::Delete, url)
497}
498
499pub fn connect<T: Into<URL>>(url: T) -> Request {
502 Request::new(Method::Connect, url)
503}
504
505pub fn options<T: Into<URL>>(url: T) -> Request {
508 Request::new(Method::Options, url)
509}
510
511pub fn trace<T: Into<URL>>(url: T) -> Request {
514 Request::new(Method::Trace, url)
515}
516
517pub fn patch<T: Into<URL>>(url: T) -> Request {
520 Request::new(Method::Patch, url)
521}
522
523const DOWNLOAD_BUFFER_SIZEE: usize = 4096;
524async fn copy_with_progress<R: ?Sized, W: ?Sized>(
525 reader: &mut R,
526 writer: &mut W,
527 total: u64,
528 f: impl Fn(u64, u64),
529) -> Result<u64, std::io::Error>
530where
531 R: tokio::io::AsyncRead + std::marker::Unpin,
532 W: tokio::io::AsyncWrite + std::marker::Unpin,
533{
534 let mut buffer = vec![0u8; DOWNLOAD_BUFFER_SIZEE];
535 let mut total_len = 0u64;
536 while let Ok(len) = reader.read_exact(&mut buffer).await {
537 writer.write_all(&buffer[0..len]).await?;
538 total_len += len as u64;
539 f(total, total_len);
540 writer.write_all(&buffer[0..len]).await?;
541 if len == 0 || total_len == total || len < DOWNLOAD_BUFFER_SIZEE {
542 break;
543 }
544 }
545 Ok(total_len)
546}
547
548
549#[cfg(test)]
550mod parsing_tests {
551
552 use std::collections::HashMap;
553
554 use super::{ParsedRequest, get};
555
556 #[test]
557 fn test_headers() {
558 let mut headers = HashMap::new();
559 headers.insert("foo".to_string(), "bar".to_string());
560 headers.insert("foo".to_string(), "baz".to_string());
561
562 let req = get("http://www.example.org/test/res").with_headers(headers.clone());
563
564 assert_eq!(req.headers, headers);
565 }
566
567 #[test]
568 fn test_multiple_params() {
569 let req = get("http://www.example.org/test/res")
570 .with_param("foo", "bar")
571 .with_param("asd", "qwe");
572 let req = ParsedRequest::new(req).unwrap();
573 assert_eq!(&req.url.path_and_query, "/test/res?foo=bar&asd=qwe");
574 }
575
576 #[test]
577 fn test_domain() {
578 let req = get("http://www.example.org/test/res").with_param("foo", "bar");
579 let req = ParsedRequest::new(req).unwrap();
580 assert_eq!(&req.url.host, "www.example.org");
581 }
582
583 #[test]
584 fn test_protocol() {
585 let req =
586 ParsedRequest::new(get("http://www.example.org/").with_param("foo", "bar")).unwrap();
587 assert!(!req.url.https);
588 let req =
589 ParsedRequest::new(get("https://www.example.org/").with_param("foo", "bar")).unwrap();
590 assert!(req.url.https);
591 }
592}
593
594#[cfg(all(test))]
595mod encoding_tests {
596 use super::{ParsedRequest, get};
597
598 #[test]
599 fn test_with_param() {
600 let req = get("http://www.example.org").with_param("foo", "bar");
601 let req = ParsedRequest::new(req).unwrap();
602 assert_eq!(&req.url.path_and_query, "/?foo=bar");
603
604 let req = get("http://www.example.org").with_param("ówò", "what's this? 👀");
605 let req = ParsedRequest::new(req).unwrap();
606 assert_eq!(
607 &req.url.path_and_query,
608 "/?%C3%B3w%C3%B2=what%27s%20this%3F%20%F0%9F%91%80"
609 );
610 }
611
612 #[test]
613 fn test_on_creation() {
614 let req = ParsedRequest::new(get("http://www.example.org/?foo=bar#baz")).unwrap();
615 assert_eq!(&req.url.path_and_query, "/?foo=bar");
616
617 let req = ParsedRequest::new(get("http://www.example.org/?ówò=what's this? 👀")).unwrap();
618 assert_eq!(
619 &req.url.path_and_query,
620 "/?%C3%B3w%C3%B2=what%27s%20this?%20%F0%9F%91%80"
621 );
622 }
623}