diff --git a/Cargo.lock b/Cargo.lock index a7515f0..5bc715d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,9 +25,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e89b6941c2d1a7045538884d6e760ccfffdf8e1ffc2613d8efa74305e1f3752" +checksum = "0f0e249228c6ad2d240c2dc94b714d711629d52bad946075d8e9b2f5391f0703" dependencies = [ "bindgen", "cc", @@ -75,12 +75,13 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "cc" -version = "1.1.6" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" +checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" dependencies = [ "jobserver", "libc", + "shlex", ] [[package]] @@ -111,9 +112,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.50" +version = "0.1.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" dependencies = [ "cc", ] @@ -130,15 +131,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "dunce" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" @@ -153,7 +154,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -206,12 +207,12 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "http_req" -version = "0.11.1" +version = "0.12.0" dependencies = [ "native-tls", "rustls", @@ -254,9 +255,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.156" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "a5f43f184355eefb8d17fc948dbecf6c13be3c141f20d834ae842193a448c72a" [[package]] name = "libloading" @@ -417,9 +418,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -456,7 +457,7 @@ dependencies = [ "libc", "spin", "untrusted", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -475,7 +476,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -495,9 +496,9 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" dependencies = [ "base64", "rustls-pki-types", @@ -505,9 +506,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" @@ -527,7 +528,7 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -573,9 +574,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.72" +version = "2.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" dependencies = [ "proc-macro2", "quote", @@ -584,14 +585,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand", + "once_cell", "rustix", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -673,6 +675,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 6f5991e..ee22a3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http_req" -version = "0.11.1" +version = "0.12.0" license = "MIT" description = "simple and lightweight HTTP client with built-in HTTPS support" repository = "https://github.com/jayjamesjay/http_req" diff --git a/README.md b/README.md index ed16c20..f23c88d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,15 @@ # http_req > [!CAUTION] -> v0.11.0 introduces major changes to design of `RequestBuilder` and `Request`. Please review [documentation](https://docs.rs/http_req/0.11.0/http_req/) before migrating from previous versions. +> v0.12.0 replaces `RequestBuilder` with `RequestMessage`. Please review [documentation](https://docs.rs/http_req/0.12.0/http_req/) before migrating from previous versions. [![Rust](https://github.com/jayjamesjay/http_req/actions/workflows/rust.yml/badge.svg)](https://github.com/jayjamesjay/http_req/actions/workflows/rust.yml) -[![Crates.io](https://img.shields.io/badge/crates.io-v0.11.1-orange.svg?longCache=true)](https://crates.io/crates/http_req) -[![Docs.rs](https://docs.rs/http_req/badge.svg)](https://docs.rs/http_req/0.11.1/http_req/) +[![Crates.io](https://img.shields.io/badge/crates.io-v0.12.0-orange.svg?longCache=true)](https://crates.io/crates/http_req) +[![Docs.rs](https://docs.rs/http_req/badge.svg)](https://docs.rs/http_req/0.12.0/http_req/) Simple and lightweight HTTP client with built-in HTTPS support. +- HTTP and HTTPS via [rust-native-tls](https://github.com/sfackler/rust-native-tls) (or optionally [rus-tls](https://crates.io/crates/rustls)) +- Small binary size (less than 0.7 MB for basic GET request) +- Minimal amount of dependencies ## Requirements http_req by default uses [rust-native-tls](https://github.com/sfackler/rust-native-tls), @@ -32,7 +35,7 @@ Take a look at [more examples](https://github.com/jayjamesjay/http_req/tree/mast In order to use `http_req` with `rustls` in your project, add the following lines to `Cargo.toml`: ```toml [dependencies] -http_req = {version="^0.11", default-features = false, features = ["rust-tls"]} +http_req = {version="^0.12", default-features = false, features = ["rust-tls"]} ``` ## License diff --git a/examples/request_builder_get.rs b/examples/advanced_request_get.rs similarity index 89% rename from examples/request_builder_get.rs rename to examples/advanced_request_get.rs index e52be18..6b60897 100644 --- a/examples/request_builder_get.rs +++ b/examples/advanced_request_get.rs @@ -1,5 +1,5 @@ use http_req::{ - request::RequestBuilder, + request::RequestMessage, response::Response, stream::{self, Stream}, uri::Uri, @@ -19,12 +19,12 @@ fn main() { let mut body = Vec::new(); // Prepares a request message. - let request_msg = RequestBuilder::new(&addr) + let request_msg = RequestMessage::new(&addr) .header("Connection", "Close") .parse(); // Connects to a server. Uses information from `addr`. - let mut stream = Stream::new(&addr, Some(Duration::from_secs(60))).unwrap(); + let mut stream = Stream::connect(&addr, Some(Duration::from_secs(60))).unwrap(); stream = Stream::try_to_https(stream, &addr, None).unwrap(); // Makes a request to server. Sends the prepared message. diff --git a/src/chunked.rs b/src/chunked.rs index 9b4616e..25ac72d 100644 --- a/src/chunked.rs +++ b/src/chunked.rs @@ -175,7 +175,7 @@ fn is_ascii_space(b: u8) -> bool { } } -fn parse_hex_uint(data: Vec) -> Result { +fn parse_hex_uint<'a>(data: Vec) -> Result { let mut n = 0usize; for (i, v) in data.iter().enumerate() { if i == 16 { diff --git a/src/error.rs b/src/error.rs index 7168732..4e54570 100644 --- a/src/error.rs +++ b/src/error.rs @@ -29,13 +29,13 @@ impl fmt::Display for ParseErr { use self::ParseErr::*; let err = match self { - Utf8(_) => "invalid character", - Int(_) => "cannot parse number", - Invalid => "invalid value", - Empty => "nothing to parse", - StatusErr => "status line contains invalid values", - HeadersErr => "headers contain invalid values", - UriErr => "uri contains invalid characters", + Utf8(_) => "Invalid character", + Int(_) => "Cannot parse number", + Invalid => "Invalid value", + Empty => "Nothing to parse", + StatusErr => "Status line contains invalid values", + HeadersErr => "Headers contain invalid values", + UriErr => "URI contains invalid characters", }; write!(f, "ParseErr: {}", err) } @@ -57,8 +57,9 @@ impl From for ParseErr { pub enum Error { IO(io::Error), Parse(ParseErr), - Timeout(mpsc::RecvTimeoutError), + Timeout, Tls, + Thread, } impl error::Error for Error { @@ -68,8 +69,7 @@ impl error::Error for Error { match self { IO(e) => Some(e), Parse(e) => Some(e), - Timeout(e) => Some(e), - Tls => None, + Timeout | Tls | Thread => None, } } } @@ -81,27 +81,14 @@ impl fmt::Display for Error { let err = match self { IO(_) => "IO error", Parse(err) => return err.fmt(f), - Timeout(_) => "Timeout error", + Timeout => "Timeout error", Tls => "TLS error", + Thread => "Thread communication error", }; write!(f, "Error: {}", err) } } -#[cfg(feature = "native-tls")] -impl From for Error { - fn from(_e: native_tls::Error) -> Self { - Error::Tls - } -} - -#[cfg(feature = "native-tls")] -impl From> for Error { - fn from(_e: native_tls::HandshakeError) -> Self { - Error::Tls - } -} - impl From for Error { fn from(e: io::Error) -> Self { Error::IO(e) @@ -121,8 +108,8 @@ impl From for Error { } impl From for Error { - fn from(e: mpsc::RecvTimeoutError) -> Self { - Error::Timeout(e) + fn from(_e: mpsc::RecvTimeoutError) -> Self { + Error::Timeout } } @@ -132,3 +119,23 @@ impl From for Error { Error::Tls } } + +#[cfg(feature = "native-tls")] +impl From for Error { + fn from(_e: native_tls::Error) -> Self { + Error::Tls + } +} + +#[cfg(feature = "native-tls")] +impl From> for Error { + fn from(_e: native_tls::HandshakeError) -> Self { + Error::Tls + } +} + +impl From> for Error { + fn from(_e: mpsc::SendError) -> Self { + Error::Thread + } +} diff --git a/src/lib.rs b/src/lib.rs index 2de2c9e..77ad98e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ //! Simple HTTP client with built-in HTTPS support. -//! +//! //! By default uses [rust-native-tls](https://github.com/sfackler/rust-native-tls), //! which relies on TLS framework provided by OS on Windows and macOS, and OpenSSL //! on all other platforms. But it also supports [rus-tls](https://crates.io/crates/rustls). diff --git a/src/request.rs b/src/request.rs index c079892..3add606 100644 --- a/src/request.rs +++ b/src/request.rs @@ -7,6 +7,7 @@ use crate::{ uri::Uri, }; use std::{ + convert::TryFrom, fmt, io::{BufReader, Write}, path::Path, @@ -17,6 +18,7 @@ use std::{ const CR_LF: &str = "\r\n"; const DEFAULT_REQ_TIMEOUT: u64 = 60 * 60; +const DEFAULT_CALL_TIMEOUT: u64 = 60; /// HTTP request methods #[derive(Debug, PartialEq, Clone, Copy)] @@ -61,7 +63,7 @@ pub enum HttpVersion { } impl HttpVersion { - pub const fn as_str(self) -> &'static str { + pub const fn as_str(&self) -> &str { use self::HttpVersion::*; match self { @@ -78,21 +80,67 @@ impl fmt::Display for HttpVersion { } } -/// Raw HTTP request that can be sent to any stream +pub struct RequestBuilder {} + +#[deprecated( + since = "0.12.0", + note = "RequestBuilder was replaced with RequestMessage" +)] +impl<'a> RequestBuilder { + pub fn new(uri: &'a Uri<'a>) -> RequestMessage<'a> { + RequestMessage::new(uri) + } +} + +/// Allows to control redirects +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum RedirectPolicy { + /// Follows redirect if limit is greater than 0. + Limit(usize), + /// Runs functions `F` to determine if redirect should be followed. + Custom(F), +} + +impl bool> RedirectPolicy { + /// Checks the policy againt specified conditions. + /// Returns `true` if redirect should be followed. + pub fn follow(&mut self) -> bool { + use self::RedirectPolicy::*; + + match self { + Limit(limit) => match limit { + 0 => false, + _ => { + *limit = *limit - 1; + true + } + }, + Custom(func) => func(), + } + } +} + +impl bool> Default for RedirectPolicy { + fn default() -> Self { + RedirectPolicy::Limit(5) + } +} + +/// Raw HTTP request message that can be sent to any stream /// /// # Examples /// ``` /// use std::convert::TryFrom; -/// use http_req::{request::RequestBuilder, uri::Uri}; +/// use http_req::{request::RequestMessage, uri::Uri}; /// /// let addr: Uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// -/// let mut request_msg = RequestBuilder::new(&addr) +/// let mut request_msg = RequestMessage::new(&addr) /// .header("Connection", "Close") /// .parse(); /// ``` #[derive(Clone, Debug, PartialEq)] -pub struct RequestBuilder<'a> { +pub struct RequestMessage<'a> { uri: &'a Uri<'a>, method: Method, version: HttpVersion, @@ -100,21 +148,21 @@ pub struct RequestBuilder<'a> { body: Option<&'a [u8]>, } -impl<'a> RequestBuilder<'a> { - /// Creates a new `RequestBuilder` with default parameters +impl<'a> RequestMessage<'a> { + /// Creates a new `RequestMessage` with default parameters /// /// # Examples /// ``` /// use std::convert::TryFrom; - /// use http_req::{request::RequestBuilder, uri::Uri}; + /// use http_req::{request::RequestMessage, uri::Uri}; /// /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - /// let request_builder = RequestBuilder::new(&addr) + /// let request_msg = RequestMessage::new(&addr) /// .header("Connection", "Close"); /// ``` - pub fn new(uri: &'a Uri<'a>) -> RequestBuilder<'a> { - RequestBuilder { + pub fn new(uri: &'a Uri<'a>) -> RequestMessage<'a> { + RequestMessage { headers: Headers::default_http(uri), uri, method: Method::GET, @@ -128,11 +176,11 @@ impl<'a> RequestBuilder<'a> { /// # Examples /// ``` /// use std::convert::TryFrom; - /// use http_req::{request::{RequestBuilder, Method}, uri::Uri}; + /// use http_req::{request::{RequestMessage, Method}, uri::Uri}; /// /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - /// let request_builder = RequestBuilder::new(&addr) + /// let request_msg = RequestMessage::new(&addr) /// .method(Method::HEAD); /// ``` pub fn method(&mut self, method: T) -> &mut Self @@ -148,11 +196,11 @@ impl<'a> RequestBuilder<'a> { /// # Examples /// ``` /// use std::convert::TryFrom; - /// use http_req::{request::{RequestBuilder, HttpVersion}, uri::Uri}; + /// use http_req::{request::{RequestMessage, HttpVersion}, uri::Uri}; /// /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - /// let request_builder = RequestBuilder::new(&addr) + /// let request_msg = RequestMessage::new(&addr) /// .version(HttpVersion::Http10); /// ``` pub fn version(&mut self, version: T) -> &mut Self @@ -168,7 +216,7 @@ impl<'a> RequestBuilder<'a> { /// # Examples /// ``` /// use std::convert::TryFrom; - /// use http_req::{request::RequestBuilder, response::Headers, uri::Uri}; + /// use http_req::{request::RequestMessage, response::Headers, uri::Uri}; /// /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// @@ -178,7 +226,7 @@ impl<'a> RequestBuilder<'a> { /// headers.insert("Host", "rust-lang.org"); /// headers.insert("Connection", "Close"); /// - /// let request_builder = RequestBuilder::new(&addr) + /// let request_msg = RequestMessage::new(&addr) /// .headers(headers); /// ``` pub fn headers(&mut self, headers: T) -> &mut Self @@ -194,11 +242,11 @@ impl<'a> RequestBuilder<'a> { /// # Examples /// ``` /// use std::convert::TryFrom; - /// use http_req::{request::RequestBuilder, response::Headers, uri::Uri}; + /// use http_req::{request::RequestMessage, response::Headers, uri::Uri}; /// /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - /// let request_builder = RequestBuilder::new(&addr) + /// let request_msg = RequestMessage::new(&addr) /// .header("Connection", "Close"); /// ``` pub fn header(&mut self, key: &T, val: &U) -> &mut Self @@ -215,12 +263,12 @@ impl<'a> RequestBuilder<'a> { /// # Examples /// ``` /// use std::convert::TryFrom; - /// use http_req::{request::{RequestBuilder, Method}, response::Headers, uri::Uri}; + /// use http_req::{request::{RequestMessage, Method}, response::Headers, uri::Uri}; /// /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// const BODY: &[u8; 27] = b"field1=value1&field2=value2"; /// - /// let request_builder = RequestBuilder::new(&addr) + /// let request_msg = RequestMessage::new(&addr) /// .method(Method::POST) /// .body(BODY) /// .header("Content-Length", &BODY.len()) @@ -231,16 +279,16 @@ impl<'a> RequestBuilder<'a> { self } - /// Parses the request message for this `RequestBuilder` + /// Parses the request message for this `RequestMessage` /// /// # Examples /// ``` /// use std::convert::TryFrom; - /// use http_req::{request::RequestBuilder, uri::Uri}; + /// use http_req::{request::RequestMessage, uri::Uri}; /// /// let addr: Uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - /// let mut request_msg = RequestBuilder::new(&addr) + /// let mut request_msg = RequestMessage::new(&addr) /// .header("Connection", "Close") /// .parse(); /// ``` @@ -261,8 +309,8 @@ impl<'a> RequestBuilder<'a> { let mut request_msg = (request_line + &headers + CR_LF).as_bytes().to_vec(); - if let Some(b) = &self.body { - request_msg.extend(*b); + if let Some(b) = self.body { + request_msg.extend(b); } request_msg @@ -288,7 +336,8 @@ impl<'a> RequestBuilder<'a> { /// #[derive(Clone, Debug, PartialEq)] pub struct Request<'a> { - inner: RequestBuilder<'a>, + messsage: RequestMessage<'a>, + redirect_policy: RedirectPolicy bool>, connect_timeout: Option, read_timeout: Option, write_timeout: Option, @@ -309,14 +358,15 @@ impl<'a> Request<'a> { /// let request = Request::new(&uri); /// ``` pub fn new(uri: &'a Uri) -> Request<'a> { - let mut builder = RequestBuilder::new(&uri); - builder.header("Connection", "Close"); + let mut message = RequestMessage::new(&uri); + message.header("Connection", "Close"); Request { - inner: builder, - connect_timeout: Some(Duration::from_secs(60)), - read_timeout: Some(Duration::from_secs(60)), - write_timeout: Some(Duration::from_secs(60)), + messsage: message, + redirect_policy: RedirectPolicy::default(), + connect_timeout: Some(Duration::from_secs(DEFAULT_CALL_TIMEOUT)), + read_timeout: Some(Duration::from_secs(DEFAULT_CALL_TIMEOUT)), + write_timeout: Some(Duration::from_secs(DEFAULT_CALL_TIMEOUT)), timeout: Duration::from_secs(DEFAULT_REQ_TIMEOUT), root_cert_file_pem: None, } @@ -338,7 +388,7 @@ impl<'a> Request<'a> { where Method: From, { - self.inner.method(method); + self.messsage.method(method); self } @@ -359,7 +409,7 @@ impl<'a> Request<'a> { where HttpVersion: From, { - self.inner.version(version); + self.messsage.version(version); self } @@ -385,7 +435,7 @@ impl<'a> Request<'a> { where Headers: From, { - self.inner.headers(headers); + self.messsage.headers(headers); self } @@ -406,7 +456,7 @@ impl<'a> Request<'a> { T: ToString + ?Sized, U: ToString + ?Sized, { - self.inner.header(key, val); + self.messsage.header(key, val); self } @@ -426,7 +476,7 @@ impl<'a> Request<'a> { /// .body(body); /// ``` pub fn body(&mut self, body: &'a [u8]) -> &mut Self { - self.inner.body(body); + self.messsage.body(body); self } @@ -552,6 +602,26 @@ impl<'a> Request<'a> { self } + /// Sets the redirect policy for the request. + /// + /// # Examples + /// ``` + /// use http_req::{request::{Request, RedirectPolicy}, uri::Uri}; + /// use std::{time::Duration, convert::TryFrom, path::Path}; + /// + /// let uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); + /// + /// let request = Request::new(&uri) + /// .redirect_policy(RedirectPolicy::Limit(5)); + /// ``` + pub fn redirect_policy(&mut self, policy: T) -> &mut Self + where + RedirectPolicy bool>: From, + { + self.redirect_policy = RedirectPolicy::from(policy); + self + } + /// Sends the HTTP request and returns `Response`. /// /// Creates `TcpStream` (and wraps it with `TlsStream` if needed). Writes request message @@ -567,18 +637,18 @@ impl<'a> Request<'a> { /// /// let response = Request::new(&uri).send(&mut writer).unwrap(); /// ``` - pub fn send(&self, writer: &mut T) -> Result + pub fn send(&mut self, writer: &mut T) -> Result where T: Write, { // Set up a stream. - let mut stream = Stream::new(self.inner.uri, self.connect_timeout)?; + let mut stream = Stream::connect(self.messsage.uri, self.connect_timeout)?; stream.set_read_timeout(self.read_timeout)?; stream.set_write_timeout(self.write_timeout)?; - stream = Stream::try_to_https(stream, self.inner.uri, self.root_cert_file_pem)?; + stream = Stream::try_to_https(stream, self.messsage.uri, self.root_cert_file_pem)?; // Send the request message to stream. - let request_msg = self.inner.parse(); + let request_msg = self.messsage.parse(); stream.write_all(&request_msg)?; // Set up variables @@ -592,7 +662,7 @@ impl<'a> Request<'a> { thread::spawn(move || { buf_reader.send_head(&sender); - let params: Vec<&str> = receiver_supp.recv().unwrap(); + let params: Vec<&str> = receiver_supp.recv().unwrap_or(Vec::new()); if params.contains(&"non-empty") { if params.contains(&"chunked") { let mut buf_reader = ChunkReader::from(buf_reader); @@ -605,25 +675,28 @@ impl<'a> Request<'a> { // Receive and process `head` of the response. raw_response_head.receive(&receiver, deadline)?; - let response = Response::from_head(&raw_response_head)?; - let content_len = response.content_len().unwrap_or(1); - let encoding = response.headers().get("Transfer-Encoding"); - let mut params = Vec::with_capacity(5); - if let Some(encode) = encoding { - if encode == "chunked" { - params.push("chunked"); - } - } + if response.status_code().is_redirect() && self.redirect_policy.follow() { + if let Some(location) = response.headers().get("Location") { + let mut raw_uri = location.to_string(); + let uri = if Uri::is_relative(&raw_uri) { + self.messsage.uri.from_relative(&mut raw_uri) + } else { + Uri::try_from(raw_uri.as_str()) + }?; - if content_len > 0 && self.inner.method != Method::HEAD { - params.push("non-empty"); + return Request::new(&uri) + .redirect_policy(self.redirect_policy) + .send(writer); + } } - sender_supp.send(params).unwrap(); + let params = response.basic_info(&self.messsage.method).to_vec(); + sender_supp.send(params)?; // Receive and process `body` of the response. + let content_len = response.content_len().unwrap_or(1); if content_len > 0 { writer.receive_all(&receiver, deadline)?; } @@ -715,22 +788,22 @@ mod tests { } #[test] - fn request_b_new() { - RequestBuilder::new(&Uri::try_from(URI).unwrap()); - RequestBuilder::new(&Uri::try_from(URI_S).unwrap()); + fn request_m_new() { + RequestMessage::new(&Uri::try_from(URI).unwrap()); + RequestMessage::new(&Uri::try_from(URI_S).unwrap()); } #[test] - fn request_b_method() { + fn request_m_method() { let uri = Uri::try_from(URI).unwrap(); - let mut req = RequestBuilder::new(&uri); + let mut req = RequestMessage::new(&uri); let req = req.method(Method::HEAD); assert_eq!(req.method, Method::HEAD); } #[test] - fn request_b_headers() { + fn request_m_headers() { let mut headers = Headers::new(); headers.insert("Accept-Charset", "utf-8"); headers.insert("Accept-Language", "en-US"); @@ -738,16 +811,16 @@ mod tests { headers.insert("Connection", "Close"); let uri = Uri::try_from(URI).unwrap(); - let mut req = RequestBuilder::new(&uri); + let mut req = RequestMessage::new(&uri); let req = req.headers(headers.clone()); assert_eq!(req.headers, headers); } #[test] - fn request_b_header() { + fn request_m_header() { let uri = Uri::try_from(URI).unwrap(); - let mut req = RequestBuilder::new(&uri); + let mut req = RequestMessage::new(&uri); let k = "Connection"; let v = "Close"; @@ -761,18 +834,18 @@ mod tests { } #[test] - fn request_b_body() { + fn request_m_body() { let uri = Uri::try_from(URI).unwrap(); - let mut req = RequestBuilder::new(&uri); + let mut req = RequestMessage::new(&uri); let req = req.body(&BODY); assert_eq!(req.body, Some(BODY.as_ref())); } #[test] - fn request_b_parse() { + fn request_m_parse() { let uri = Uri::try_from(URI).unwrap(); - let req = RequestBuilder::new(&uri); + let req = RequestMessage::new(&uri); const DEFAULT_MSG: &str = "GET /std/string/index.html HTTP/1.1\r\n\ Host: doc.rust-lang.org\r\n\r\n"; @@ -800,7 +873,7 @@ mod tests { let mut req = Request::new(&uri); req.method(Method::HEAD); - assert_eq!(req.inner.method, Method::HEAD); + assert_eq!(req.messsage.method, Method::HEAD); } #[test] @@ -815,7 +888,7 @@ mod tests { let mut req = Request::new(&uri); let req = req.headers(headers.clone()); - assert_eq!(req.inner.headers, headers); + assert_eq!(req.messsage.headers, headers); } #[test] @@ -832,7 +905,7 @@ mod tests { let req = req.header(k, v); - assert_eq!(req.inner.headers, expect_headers); + assert_eq!(req.messsage.headers, expect_headers); } #[test] @@ -841,7 +914,7 @@ mod tests { let mut req = Request::new(&uri); let req = req.body(&BODY); - assert_eq!(req.inner.body, Some(BODY.as_ref())); + assert_eq!(req.messsage.body, Some(BODY.as_ref())); } #[test] diff --git a/src/response.rs b/src/response.rs index 895e6d6..68aff81 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,6 +1,7 @@ //! parsing server response use crate::{ error::{Error, ParseErr}, + request::Method, uri::Uri, }; use std::{ @@ -60,7 +61,10 @@ impl Response { /// /// let response = Response::try_from(RESPONSE, &mut body).unwrap(); /// ``` - pub fn try_from(res: &[u8], writer: &mut T) -> Result { + pub fn try_from(res: &[u8], writer: &mut T) -> Result + where + T: Write, + { if res.is_empty() { Err(Error::Parse(ParseErr::Empty)) } else { @@ -152,7 +156,7 @@ impl Response { /// let response = Response::try_from(RESPONSE, &mut body).unwrap(); /// let headers = response.headers(); /// ``` - pub fn headers(&self) -> &Headers { + pub const fn headers(&self) -> &Headers { &self.headers } @@ -178,6 +182,31 @@ impl Response { .get("Content-Length") .and_then(|len| len.parse().ok()) } + + /// Checks if Transfer-Encoding includes "chunked". + pub fn is_chunked(&self) -> bool { + self.headers() + .get("Transfer-Encoding") + .is_some_and(|encodings| encodings.contains("chunked")) + } + + /// Returns basic information about the response as an array, including: + /// - chunked -> Transfer-Encoding includes "chunked" + /// - non-empty -> Content-Length is greater than 0 (or unknown) and method is not HEAD + pub fn basic_info<'a>(&self, method: &Method) -> [&'a str; 2] { + let mut params = [""; 2]; + let content_len = self.content_len().unwrap_or(1); + + if self.is_chunked() { + params[0] = "chunked"; + } + + if content_len > 0 && method != &Method::HEAD { + params[1] = "non-empty"; + } + + params + } } /// Status of HTTP response @@ -502,7 +531,7 @@ impl StatusCode { /// const code: StatusCode = StatusCode::new(200); /// assert_eq!(code.reason(), Some("OK")) /// ``` - pub const fn reason(self) -> Option<&'static str> { + pub const fn reason(&self) -> Option<&str> { let reason = match self.0 { 100 => "Continue", 101 => "Switching Protocols", diff --git a/src/stream.rs b/src/stream.rs index 1afe877..e9840c6 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -13,6 +13,7 @@ const BUF_SIZE: usize = 16 * 1000; /// Wrapper around TCP stream for HTTP and HTTPS protocols. /// Allows to perform common operations on underlying stream. +#[derive(Debug)] pub enum Stream { Http(TcpStream), Https(Conn), @@ -20,7 +21,16 @@ pub enum Stream { impl Stream { /// Opens a TCP connection to a remote host with a connection timeout (if specified). + #[deprecated( + since = "0.12.0", + note = "Stream::new(uri, connect_timeout) was replaced with Stream::connect(uri, connect_timeout)" + )] pub fn new(uri: &Uri, connect_timeout: Option) -> Result { + Stream::connect(uri, connect_timeout) + } + + /// Opens a TCP connection to a remote host with a connection timeout (if specified). + pub fn connect(uri: &Uri, connect_timeout: Option) -> Result { let host = uri.host().unwrap_or(""); let port = uri.corr_port(); @@ -119,7 +129,7 @@ where { fn send_head(&mut self, sender: &Sender>) { let buf = read_head(self); - sender.send(buf).unwrap(); + sender.send(buf).unwrap_or(()); } fn send_all(&mut self, sender: &Sender>) { @@ -130,7 +140,9 @@ where Ok(0) | Err(_) => break, Ok(len) => { let filled_buf = buf[..len].to_vec(); - sender.send(filled_buf).unwrap(); + if let Err(_) = sender.send(filled_buf) { + break; + } } } } @@ -165,30 +177,18 @@ where receiver: &Receiver>, deadline: Instant, ) -> Result<(), Error> { - let mut result = Ok(()); - execute_with_deadline(deadline, |remaining_time| { - let mut is_complete = false; - let data_read = match receiver.recv_timeout(remaining_time) { Ok(data) => data, - Err(e) => { - if e == RecvTimeoutError::Timeout { - result = Err(Error::Timeout(RecvTimeoutError::Timeout)); - } - return true; - } + Err(e) => match e { + RecvTimeoutError::Timeout => return Err(Error::Timeout), + RecvTimeoutError::Disconnected => return Ok(true), + }, }; - if let Err(e) = self.write_all(&data_read).map_err(|e| Error::IO(e)) { - result = Err(e); - is_complete = true; - } - - is_complete - }); - - Ok(result?) + self.write_all(&data_read).map_err(|e| Error::IO(e))?; + Ok(false) + }) } } @@ -226,31 +226,39 @@ where /// Exexcutes a function in a loop until operation is completed or deadline is exceeded. /// /// It checks if a timeout was exceeded every iteration, therefore it limits -/// how many time a specific function can be called before deadline. -/// For the `execute_with_deadline` to meet the deadline, each call -/// to `func` needs finish before the deadline. -/// +/// how many time a specific function can be called before deadline. +/// For the `execute_with_deadline` to meet the deadline, each call +/// to `func` needs finish before the deadline. +/// /// Key information about function `func`: /// - is provided with information about remaining time /// - must ensure that its execution will not take more time than specified in `remaining_time` -/// - needs to return `true` when the operation is complete -pub fn execute_with_deadline(deadline: Instant, mut func: F) +/// - needs to return `Some(true)` when the operation is complete, and `Some(false)` - when operation is in progress +pub fn execute_with_deadline(deadline: Instant, mut func: F) -> Result<(), Error> where - F: FnMut(Duration) -> bool, + F: FnMut(Duration) -> Result, { loop { let now = Instant::now(); let remaining_time = deadline - now; - if deadline < now || func(remaining_time) == true { - break; + if deadline < now { + return Err(Error::Timeout); + } + + match func(remaining_time) { + Ok(true) => break, + Ok(false) => continue, + Err(e) => return Err(e), } } + + Ok(()) } /// Reads the head of HTTP response from `reader`. /// -/// Reads from `reader` (line by line) until a blank line is identified, +/// Reads from `reader` (line by line) until a blank line is identified, /// which indicates that all meta-information has been read, pub fn read_head(reader: &mut B) -> Vec where @@ -297,13 +305,13 @@ mod tests { fn stream_new() { { let uri = Uri::try_from(URI).unwrap(); - let stream = Stream::new(&uri, None); + let stream = Stream::connect(&uri, None); assert!(stream.is_ok()); } { let uri = Uri::try_from(URI).unwrap(); - let stream = Stream::new(&uri, Some(TIMEOUT)); + let stream = Stream::connect(&uri, Some(TIMEOUT)); assert!(stream.is_ok()); } @@ -313,7 +321,7 @@ mod tests { fn stream_try_to_https() { { let uri = Uri::try_from(URI_S).unwrap(); - let stream = Stream::new(&uri, None).unwrap(); + let stream = Stream::connect(&uri, None).unwrap(); let https_stream = Stream::try_to_https(stream, &uri, None); assert!(https_stream.is_ok()); @@ -326,7 +334,7 @@ mod tests { } { let uri = Uri::try_from(URI).unwrap(); - let stream = Stream::new(&uri, None).unwrap(); + let stream = Stream::connect(&uri, None).unwrap(); let https_stream = Stream::try_to_https(stream, &uri, None); assert!(https_stream.is_ok()); @@ -343,7 +351,7 @@ mod tests { fn stream_set_read_timeot() { { let uri = Uri::try_from(URI).unwrap(); - let mut stream = Stream::new(&uri, None).unwrap(); + let mut stream = Stream::connect(&uri, None).unwrap(); stream.set_read_timeout(Some(TIMEOUT)).unwrap(); let inner_read_timeout = if let Stream::Http(inner) = stream { @@ -356,7 +364,7 @@ mod tests { } { let uri = Uri::try_from(URI_S).unwrap(); - let mut stream = Stream::new(&uri, None).unwrap(); + let mut stream = Stream::connect(&uri, None).unwrap(); stream = Stream::try_to_https(stream, &uri, None).unwrap(); stream.set_read_timeout(Some(TIMEOUT)).unwrap(); @@ -374,7 +382,7 @@ mod tests { fn stream_set_write_timeot() { { let uri = Uri::try_from(URI).unwrap(); - let mut stream = Stream::new(&uri, None).unwrap(); + let mut stream = Stream::connect(&uri, None).unwrap(); stream.set_write_timeout(Some(TIMEOUT)).unwrap(); let inner_read_timeout = if let Stream::Http(inner) = stream { @@ -387,7 +395,7 @@ mod tests { } { let uri = Uri::try_from(URI_S).unwrap(); - let mut stream = Stream::new(&uri, None).unwrap(); + let mut stream = Stream::connect(&uri, None).unwrap(); stream = Stream::try_to_https(stream, &uri, None).unwrap(); stream.set_write_timeout(Some(TIMEOUT)).unwrap(); @@ -472,17 +480,18 @@ mod tests { let star_time = Instant::now(); let deadline = star_time + TIMEOUT; - execute_with_deadline(deadline, |_| { + let timeout_err = execute_with_deadline(deadline, |_| { let sleep_time = Duration::from_millis(500); thread::sleep(sleep_time); - false + Ok(false) }); let end_time = Instant::now(); let total_time = end_time.duration_since(star_time).as_secs(); assert_eq!(total_time, TIMEOUT.as_secs()); + assert!(timeout_err.is_err()); } { let star_time = Instant::now(); @@ -492,8 +501,9 @@ mod tests { let sleep_time = Duration::from_secs(1); thread::sleep(sleep_time); - true - }); + Ok(true) + }) + .unwrap(); let end_time = Instant::now(); let total_time = end_time.duration_since(star_time).as_secs(); diff --git a/src/tls.rs b/src/tls.rs index 9cd771e..edb5732 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -18,9 +18,10 @@ use rustls_pki_types::ServerName; #[cfg(not(any(feature = "native-tls", feature = "rust-tls")))] compile_error!("one of the `native-tls` or `rust-tls` features must be enabled"); -/// Wrapper around TLS Stream, depends on selected TLS library (`S: io::Read + io::Write`): +/// Wrapper around TLS Stream, depends on selected TLS library: /// - native_tls: `TlsStream` /// - rustls: `StreamOwned` +#[derive(Debug)] pub struct Conn { #[cfg(feature = "native-tls")] stream: native_tls::TlsStream, @@ -70,7 +71,10 @@ where } } -impl io::Write for Conn { +impl io::Write for Conn +where + S: io::Read + io::Write, +{ fn write(&mut self, buf: &[u8]) -> Result { self.stream.write(buf) } diff --git a/src/uri.rs b/src/uri.rs index ca76801..c4a65ce 100644 --- a/src/uri.rs +++ b/src/uri.rs @@ -80,6 +80,11 @@ pub struct Uri<'a> { } impl<'a> Uri<'a> { + /// Returns a reference to the underlying &str. + pub fn get_ref(&self) -> &str { + self.inner + } + /// Returns scheme of this `Uri`. /// /// # Example @@ -95,7 +100,7 @@ impl<'a> Uri<'a> { } /// Returns information about the user included in this `Uri`. - /// + /// /// # Example /// ``` /// use http_req::uri::Uri; @@ -109,7 +114,7 @@ impl<'a> Uri<'a> { } /// Returns host of this `Uri`. - /// + /// /// # Example /// ``` /// use http_req::uri::Uri; @@ -123,7 +128,7 @@ impl<'a> Uri<'a> { } /// Returns host of this `Uri` to use in a header. - /// + /// /// # Example /// ``` /// use http_req::uri::Uri; @@ -140,7 +145,7 @@ impl<'a> Uri<'a> { } /// Returns port of this `Uri` - /// + /// /// # Example /// ``` /// use http_req::uri::Uri; @@ -155,7 +160,7 @@ impl<'a> Uri<'a> { /// Returns port corresponding to this `Uri`. /// Returns default port if it hasn't been set in the uri. - /// + /// /// # Example /// ``` /// use http_req::uri::Uri; @@ -177,7 +182,7 @@ impl<'a> Uri<'a> { } /// Returns path of this `Uri`. - /// + /// /// # Example /// ``` /// use http_req::uri::Uri; @@ -191,7 +196,7 @@ impl<'a> Uri<'a> { } /// Returns query of this `Uri`. - /// + /// /// # Example /// ``` /// use http_req::uri::Uri; @@ -205,7 +210,7 @@ impl<'a> Uri<'a> { } /// Returns fragment of this `Uri`. - /// + /// /// # Example /// ``` /// use http_req::uri::Uri; @@ -219,7 +224,7 @@ impl<'a> Uri<'a> { } /// Returns resource `Uri` points to. - /// + /// /// # Example /// ``` /// use http_req::uri::Uri; @@ -234,6 +239,75 @@ impl<'a> Uri<'a> { None => "/", } } + + /// Checks if &str is a relative uri. + pub fn is_relative(raw_uri: &str) -> bool { + raw_uri.starts_with("/") + || raw_uri.starts_with("?") + || raw_uri.starts_with("#") + || !raw_uri.contains(":") + } + + /// Creates a new `Uri` from current uri and relative uri. + /// Transforms the relative uri into an absolute uri. + pub fn from_relative(&'a self, relative_uri: &'a mut String) -> Result, Error> { + let inner_uri = self.inner; + let mut resource = self.resource().to_string(); + + resource = match &relative_uri.get(..1) { + Some("#") => Uri::add_part_start(&resource, relative_uri, "#"), + Some("?") => Uri::add_part_start(&self.path().unwrap_or("/"), relative_uri, "?"), + Some("/") => Uri::add_part_start(&resource, relative_uri, "/"), + Some(_) | None => Uri::add_part_end(&resource, relative_uri, "/"), + }; + + *relative_uri = if let Some(p) = self.path { + inner_uri[..p.start].to_string() + &resource + } else { + inner_uri.trim_end_matches("/").to_string() + &resource + }; + + Uri::try_from(relative_uri.as_str()) + } + + /// Adds a part at the beggining of the base. + /// Finds the first occurance of a separator in a base and the first occurance of a separator in a part. + /// Joins all chars before the separator from the base, separator and all chars after the separator from the part. + fn add_part_start(base: &str, part: &str, separator: &str) -> String { + let base_idx = base.find(separator); + Uri::add_part(base, part, separator, base_idx) + } + + /// Adds a part at the end of the base. + /// Finds the last occurance of a separator in a base and the first occurance of a separator in a part. + /// Joins all chars before the separator from the base, separator and all chars after the separator from the part. + fn add_part_end(base: &str, part: &str, separator: &str) -> String { + let base_idx = base.rfind(separator); + Uri::add_part(base, part, separator, base_idx) + } + + /// Adds a part to the base with separator in between. + /// Base index defines where part should be added. + fn add_part(base: &str, part: &str, separator: &str, base_idx: Option) -> String { + let mut output = String::new(); + let part_idx = part.find(separator); + + if let Some(idx) = base_idx { + output += &base[..idx]; + } else { + output += base; + } + + output += separator; + + if let Some(idx) = part_idx { + output += &part[idx + 1..]; + } else { + output += part; + } + + output + } } impl<'a> fmt::Display for Uri<'a> { @@ -504,6 +578,16 @@ mod tests { "[4b10:bbb0:0:d0::ba7:8001]:443", ]; + const TEST_PARTS: [&str; 7] = [ + "?query123", + "/path", + "#fragment", + "other-path", + "#paragraph", + "./foo/bar/buz", + "?users#1551", + ]; + #[test] fn remove_space() { let mut text = String::from("Hello World !"); @@ -722,6 +806,87 @@ mod tests { } } + #[test] + fn uri_is_relative() { + for i in 0..TEST_URIS.len() { + assert!(!Uri::is_relative(TEST_URIS[i])); + } + + for i in 0..TEST_PARTS.len() { + assert!(Uri::is_relative(TEST_PARTS[i])); + } + } + + #[test] + fn uri_from_relative() { + let uris: Vec<_> = TEST_URIS + .iter() + .map(|&uri| Uri::try_from(uri).unwrap()) + .collect(); + + const RESULT: [&str; 7] = [ + "https://user:info@foo.com:12/bar/baz?query123", + "file:///path", + "https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#fragment", + "mailto:John.Doe@example.com/other-path", + "https://[4b10:bbb0:0:d0::ba7:8001]:443/#paragraph", + "http://example.com/foo/bar/buz", + "https://example.com/?users#1551", + ]; + + for i in 0..RESULT.len() { + let mut uri_part = TEST_PARTS[i].to_string(); + + println!("{}", uris[i].resource()); + assert_eq!( + uris[i].from_relative(&mut uri_part).unwrap().inner, + RESULT[i] + ); + } + } + + #[test] + fn uri_add_part() { + const BASES: [&str; 2] = ["/bar/baz/fizz?query", "/bar/baz?query#some-fragment"]; + const RESULT: [&str; 2] = [ + "/bar/baz/fizz?query#another-fragment", + "/bar/baz?query#some-fragment#another-fragment", + ]; + + for i in 0..BASES.len() { + assert_eq!( + Uri::add_part(BASES[i], "#another-fragment", "#", Some(BASES[i].len())), + RESULT[i] + ); + } + } + + #[test] + fn uri_add_part_start() { + const BASES: [&str; 2] = ["/bar/baz/fizz?query", "/bar/baz?query#some-fragment"]; + const RESULT: [&str; 2] = [ + "/bar/baz/fizz?query#another-fragment", + "/bar/baz?query#another-fragment", + ]; + + for i in 0..BASES.len() { + assert_eq!( + Uri::add_part_start(BASES[i], "#another-fragment", "#"), + RESULT[i] + ); + } + } + + #[test] + fn uri_add_part_end() { + const BASES: [&str; 2] = ["/bar/baz/fizz?query", "/bar/baz?query#some-fragment"]; + const RESULT: [&str; 2] = ["/bar/baz/another", "/bar/another"]; + + for i in 0..BASES.len() { + assert_eq!(Uri::add_part_end(BASES[i], "./another", "/"), RESULT[i]); + } + } + #[test] fn uri_display() { let uris: Vec<_> = TEST_URIS