diff --git a/Cargo.lock b/Cargo.lock index 5bc715d..4b677d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.8.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae74d9bd0a7530e8afd1770739ad34b36838829d6ad61818f9230f683f5ad77" +checksum = "cdd82dba44d209fddb11c190e0a94b78651f95299598e472215667417a03ff1d" dependencies = [ "aws-lc-sys", "mirai-annotations", @@ -25,9 +25,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.20.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0e249228c6ad2d240c2dc94b714d711629d52bad946075d8e9b2f5391f0703" +checksum = "df7a4168111d7eb622a31b214057b8509c0a7e1794f44c546d742330dc793972" dependencies = [ "bindgen", "cc", @@ -46,9 +46,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bindgen" -version = "0.69.4" +version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ "bitflags", "cexpr", @@ -75,9 +75,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "cc" -version = "1.1.13" +version = "1.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" +checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" dependencies = [ "jobserver", "libc", @@ -159,9 +159,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "foreign-types" @@ -212,8 +212,9 @@ dependencies = [ [[package]] name = "http_req" -version = "0.12.0" +version = "0.13.0" dependencies = [ + "base64", "native-tls", "rustls", "rustls-pemfile", @@ -221,6 +222,7 @@ dependencies = [ "unicase", "webpki", "webpki-roots", + "zeroize", ] [[package]] @@ -255,9 +257,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.156" +version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5f43f184355eefb8d17fc948dbecf6c13be3c141f20d834ae842193a448c72a" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "libloading" @@ -328,9 +330,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "openssl" @@ -384,15 +386,15 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "prettyplease" -version = "0.2.20" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" dependencies = [ "proc-macro2", "syn", @@ -400,27 +402,27 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "regex" -version = "1.10.6" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", @@ -430,9 +432,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -441,9 +443,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "ring" @@ -468,9 +470,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ "bitflags", "errno", @@ -481,9 +483,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.12" +version = "0.23.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" dependencies = [ "aws-lc-rs", "log", @@ -496,25 +498,24 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" [[package]] name = "rustls-webpki" -version = "0.102.6" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "aws-lc-rs", "ring", @@ -524,11 +525,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -546,9 +547,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" dependencies = [ "core-foundation-sys", "libc", @@ -574,9 +575,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.74" +version = "2.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" dependencies = [ "proc-macro2", "quote", @@ -585,9 +586,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" dependencies = [ "cfg-if", "fastrand", @@ -607,9 +608,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "untrusted" @@ -647,9 +648,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.3" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" dependencies = [ "rustls-pki-types", ] diff --git a/Cargo.toml b/Cargo.toml index ee22a3e..c1c4d78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http_req" -version = "0.12.0" +version = "0.13.0" license = "MIT" description = "simple and lightweight HTTP client with built-in HTTPS support" repository = "https://github.com/jayjamesjay/http_req" @@ -12,6 +12,14 @@ edition = "2021" [dependencies] unicase = "^2.7" +base64 = "^0.22.1" +zeroize = { version = "^1.8.1", features = ["zeroize_derive"] } +native-tls = { version = "^0.2", optional = true } +rustls = { version = "^0.23", optional = true } +rustls-pemfile = { version = "^2.1", optional = true } +rustls-pki-types = { version = "^1.7", features = ["alloc"], optional = true } +webpki = { version = "^0.22", optional = true } +webpki-roots = { version = "^0.26", optional = true } [features] default = ["native-tls"] @@ -22,28 +30,3 @@ rust-tls = [ "webpki-roots", "rustls-pemfile", ] - -[dependencies.native-tls] -version = "^0.2" -optional = true - -[dependencies.rustls] -version = "^0.23" -optional = true - -[dependencies.rustls-pemfile] -version = "^2.1" -optional = true - -[dependencies.webpki] -version = "^0.22" -optional = true - -[dependencies.webpki-roots] -version = "^0.26" -optional = true - -[dependencies.rustls-pki-types] -version = "^1.7" -features = ["alloc"] -optional = true diff --git a/README.md b/README.md index f23c88d..74c45e2 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,25 @@ # http_req -> [!CAUTION] -> 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.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/) +[![Crates.io](https://img.shields.io/badge/crates.io-v0.13.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.13.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) +- Small binary size (0.7 MB for basic GET request in default configuration) - Minimal amount of dependencies ## Requirements + http_req 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). ## Example + Basic HTTP GET request + ```rust use http_req::request; @@ -31,12 +33,26 @@ fn main() { Take a look at [more examples](https://github.com/jayjamesjay/http_req/tree/master/examples) -## How to use with `rustls`: +## Usage + +### Default configuration + +In order to use `http_req` with default configuration, add the following lines to `Cargo.toml`: + +```toml +[dependencies] +http_req = "^0.13" +``` + +### Rustls + In order to use `http_req` with `rustls` in your project, add the following lines to `Cargo.toml`: + ```toml [dependencies] -http_req = {version="^0.12", default-features = false, features = ["rust-tls"]} +http_req = { version="^0.13", default-features = false, features = ["rust-tls"] } ``` ## License + Licensed under [MIT](https://github.com/jayjamesjay/http_req/blob/master/LICENSE). diff --git a/benches/bench.rs b/benches/bench.rs index bb6f26a..8ba3b9a 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -2,10 +2,30 @@ extern crate http_req; extern crate test; -use http_req::{request::Request, response::Response, uri::Uri}; +use http_req::{request::RequestMessage, response::Response, uri::Uri}; use std::{convert::TryFrom, fs::File, io::Read}; use test::Bencher; +const URI: &str = "https://www.rust-lang.org/"; +const BODY: [u8; 14] = [78, 97, 109, 101, 61, 74, 97, 109, 101, 115, 43, 74, 97, 121]; + +#[bench] +fn parse_uri(b: &mut Bencher) { + b.iter(|| Uri::try_from(URI)); +} + +#[bench] +fn parse_request(b: &mut Bencher) { + let uri = Uri::try_from(URI).unwrap(); + + b.iter(|| { + RequestMessage::new(&uri) + .header("Accept", "*/*") + .body(&BODY) + .parse(); + }); +} + #[bench] fn parse_response(b: &mut Bencher) { let mut content = Vec::new(); @@ -17,22 +37,3 @@ fn parse_response(b: &mut Bencher) { Response::try_from(&content, &mut body) }); } - -const URI: &str = "https://www.rust-lang.org/"; - -#[bench] -fn request_send(b: &mut Bencher) { - b.iter(|| { - let uri = Uri::try_from(URI).unwrap(); - let mut writer = Vec::new(); - - let res = Request::new(&uri).send(&mut writer).unwrap(); - - res - }); -} - -#[bench] -fn parse_uri(b: &mut Bencher) { - b.iter(|| Uri::try_from(URI)); -} diff --git a/examples/authentication.rs b/examples/authentication.rs new file mode 100644 index 0000000..5bdffe5 --- /dev/null +++ b/examples/authentication.rs @@ -0,0 +1,24 @@ +use http_req::{ + request::{Authentication, Request}, + uri::Uri, +}; + +fn main() { + // Container for body of a response. + let mut body = Vec::new(); + // URL of the website. + let uri = Uri::try_from("http://httpbin.org/basic-auth/foo/bar").unwrap(); + // Authentication details: username and password. + let auth = Authentication::basic("foo", "bar"); + + // Sends a HTTP GET request and processes the response. Saves body of the response to `body` variable. + let res = Request::new(&uri) + .authentication(auth) + .send(&mut body) + .unwrap(); + + //Prints details about the response. + println!("Status: {} {}", res.status_code(), res.reason()); + println!("Headers: {}", res.headers()); + println!("{}", String::from_utf8_lossy(&body)); +} diff --git a/src/request.rs b/src/request.rs index 3add606..1fef638 100644 --- a/src/request.rs +++ b/src/request.rs @@ -6,6 +6,7 @@ use crate::{ stream::{Stream, ThreadReceive, ThreadSend}, uri::Uri, }; +use base64::engine::{general_purpose::URL_SAFE, Engine}; use std::{ convert::TryFrom, fmt, @@ -15,8 +16,10 @@ use std::{ thread, time::{Duration, Instant}, }; +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; const CR_LF: &str = "\r\n"; +const DEFAULT_REDIRECT_LIMIT: usize = 5; const DEFAULT_REQ_TIMEOUT: u64 = 60 * 60; const DEFAULT_CALL_TIMEOUT: u64 = 60; @@ -34,11 +37,20 @@ pub enum Method { PATCH, } -impl fmt::Display for Method { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +impl Method { + /// Returns a string representation of a HTTP request method. + /// + /// # Examples + /// ``` + /// use http_req::request::Method; + /// + /// let method = Method::GET; + /// assert_eq!(method.as_str(), "GET"); + /// ``` + pub const fn as_str(&self) -> &str { use self::Method::*; - let method = match self { + match self { GET => "GET", HEAD => "HEAD", POST => "POST", @@ -48,9 +60,13 @@ impl fmt::Display for Method { OPTIONS => "OPTIONS", TRACE => "TRACE", PATCH => "PATCH", - }; + } + } +} - write!(f, "{}", method) +impl fmt::Display for Method { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.as_str()) } } @@ -63,6 +79,15 @@ pub enum HttpVersion { } impl HttpVersion { + /// Returns a string representation of a HTTP version. + /// + /// # Examples + /// ``` + /// use http_req::request::HttpVersion; + /// + /// let version = HttpVersion::Http10; + /// assert_eq!(version.as_str(), "HTTP/1.0"); + /// ``` pub const fn as_str(&self) -> &str { use self::HttpVersion::*; @@ -80,15 +105,101 @@ impl fmt::Display for HttpVersion { } } -pub struct RequestBuilder {} +/// Authentication details: +/// - Basic: username and password +/// - Bearer: token +#[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop)] +pub struct Authentication(AuthenticationType); -#[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) +impl Authentication { + /// Creates a new `Authentication` of type `Basic`. + /// + /// # Examples + /// ``` + /// use http_req::request::Authentication; + /// + /// let auth = Authentication::basic("foo", "bar"); + /// ``` + pub fn basic(username: &T, password: &U) -> Authentication + where + T: ToString + ?Sized, + U: ToString + ?Sized, + { + Authentication(AuthenticationType::Basic { + username: username.to_string(), + password: password.to_string(), + }) + } + + /// Creates a new `Authentication` of type `Bearer` + /// + /// # Examples + /// ``` + /// use http_req::request::Authentication; + /// + /// let auth = Authentication::bearer("secret_token"); + /// ``` + pub fn bearer(token: &T) -> Authentication + where + T: ToString + ?Sized, + { + Authentication(AuthenticationType::Bearer(token.to_string())) + } + + /// Generates a HTTP Authorization header. Returns `key` & `value` pair. + /// - Basic: uses base64 encoding on provided credentials + /// - Bearer: uses token as is + /// + /// # Examples + /// ``` + /// use http_req::request::Authentication; + /// + /// let auth = Authentication::bearer("secretToken"); + /// let (key, val) = auth.header(); + /// + /// assert_eq!(key, "Authorization"); + /// assert_eq!(val, "Bearer secretToken"); + /// ``` + pub fn header(&self) -> (String, String) { + let key = "Authorization".to_string(); + let val = String::with_capacity(200) + self.0.scheme() + " " + &self.0.credentials(); + + (key, val) + } +} + +/// Authentication types +#[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop)] +enum AuthenticationType { + Basic { username: String, password: String }, + Bearer(String), +} + +impl AuthenticationType { + /// Returns scheme + const fn scheme(&self) -> &str { + use AuthenticationType::*; + + match self { + Basic { + username: _, + password: _, + } => "Basic", + Bearer(_) => "Bearer", + } + } + + /// Returns encoded credentials + fn credentials(&self) -> Zeroizing { + use AuthenticationType::*; + + match self { + Basic { username, password } => { + let credentials = Zeroizing::new(username.to_string() + ":" + password); + Zeroizing::new(URL_SAFE.encode(credentials.as_bytes())) + } + Bearer(token) => Zeroizing::new(token.to_string()), + } } } @@ -101,10 +212,27 @@ pub enum RedirectPolicy { Custom(F), } -impl bool> RedirectPolicy { - /// Checks the policy againt specified conditions. - /// Returns `true` if redirect should be followed. - pub fn follow(&mut self) -> bool { +impl RedirectPolicy +where + F: Fn(&str) -> bool, +{ + /// Checks the policy againt specified conditions: + /// - Limit - checks if limit is greater than 0 + /// - Custom - runs functions `F` passing `uri` as parameter and returns its output + /// + /// # Examples + /// ``` + /// use http_req::request::RedirectPolicy; + /// + /// let uri: &str = "https://www.rust-lang.org/learn"; + /// + /// let mut policy_1: RedirectPolicy bool> = RedirectPolicy::Limit(5); + /// assert_eq!(policy_1.follow(&uri), true); + /// + /// let mut policy_2: RedirectPolicy bool> = RedirectPolicy::Custom(|uri| false); + /// assert_eq!(policy_2.follow(&uri), false); + /// ``` + pub fn follow(&mut self, uri: &str) -> bool { use self::RedirectPolicy::*; match self { @@ -115,14 +243,17 @@ impl bool> RedirectPolicy { true } }, - Custom(func) => func(), + Custom(func) => func(uri), } } } -impl bool> Default for RedirectPolicy { +impl Default for RedirectPolicy +where + F: Fn(&str) -> bool, +{ fn default() -> Self { - RedirectPolicy::Limit(5) + RedirectPolicy::Limit(DEFAULT_REDIRECT_LIMIT) } } @@ -258,6 +389,29 @@ impl<'a> RequestMessage<'a> { self } + /// Adds an authorization header to existing headers + /// + /// # Examples + /// ``` + /// use std::convert::TryFrom; + /// use http_req::{request::{RequestMessage, Authentication}, response::Headers, uri::Uri}; + /// + /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); + /// + /// let request_msg = RequestMessage::new(&addr) + /// .authentication(Authentication::bearer("secret456token123")); + /// ``` + pub fn authentication(&mut self, auth: T) -> &mut Self + where + Authentication: From, + { + let auth = Authentication::from(auth); + let (key, val) = auth.header(); + + self.headers.insert_raw(key, val); + self + } + /// Sets the body for request /// /// # Examples @@ -270,12 +424,11 @@ impl<'a> RequestMessage<'a> { /// /// let request_msg = RequestMessage::new(&addr) /// .method(Method::POST) - /// .body(BODY) - /// .header("Content-Length", &BODY.len()) - /// .header("Connection", "Close"); + /// .body(BODY); /// ``` pub fn body(&mut self, body: &'a [u8]) -> &mut Self { self.body = Some(body); + self.header("Content-Length", &body.len()); self } @@ -293,7 +446,7 @@ impl<'a> RequestMessage<'a> { /// .parse(); /// ``` pub fn parse(&self) -> Vec { - let request_line = format!( + let mut request_msg = format!( "{} {} {}{}", self.method, self.uri.resource(), @@ -301,14 +454,11 @@ impl<'a> RequestMessage<'a> { CR_LF ); - let headers: String = self - .headers - .iter() - .map(|(k, v)| format!("{}: {}{}", k, v, CR_LF)) - .collect(); - - let mut request_msg = (request_line + &headers + CR_LF).as_bytes().to_vec(); + for (key, val) in self.headers.iter() { + request_msg = request_msg + key + ": " + val + CR_LF; + } + let mut request_msg = (request_msg + CR_LF).as_bytes().to_vec(); if let Some(b) = self.body { request_msg.extend(b); } @@ -337,7 +487,7 @@ impl<'a> RequestMessage<'a> { #[derive(Clone, Debug, PartialEq)] pub struct Request<'a> { messsage: RequestMessage<'a>, - redirect_policy: RedirectPolicy bool>, + redirect_policy: RedirectPolicy bool>, connect_timeout: Option, read_timeout: Option, write_timeout: Option, @@ -460,6 +610,26 @@ impl<'a> Request<'a> { self } + /// Adds an authorization header to existing headers. + /// + /// # Examples + /// ``` + /// use std::convert::TryFrom; + /// use http_req::{request::{RequestMessage, Authentication}, response::Headers, uri::Uri}; + /// + /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); + /// + /// let request_msg = RequestMessage::new(&addr) + /// .authentication(Authentication::bearer("secret456token123")); + /// ``` + pub fn authentication(&mut self, auth: T) -> &mut Self + where + Authentication: From, + { + self.messsage.authentication(auth); + self + } + /// Sets the body for request. /// /// # Examples @@ -616,7 +786,7 @@ impl<'a> Request<'a> { /// ``` pub fn redirect_policy(&mut self, policy: T) -> &mut Self where - RedirectPolicy bool>: From, + RedirectPolicy bool>: From, { self.redirect_policy = RedirectPolicy::from(policy); self @@ -677,18 +847,20 @@ impl<'a> Request<'a> { raw_response_head.receive(&receiver, deadline)?; let response = Response::from_head(&raw_response_head)?; - if response.status_code().is_redirect() && self.redirect_policy.follow() { + if response.status_code().is_redirect() { 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()) - }?; - - return Request::new(&uri) - .redirect_policy(self.redirect_policy) - .send(writer); + if self.redirect_policy.follow(&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()) + }?; + + return Request::new(&uri) + .redirect_policy(self.redirect_policy) + .send(writer); + } } } @@ -765,7 +937,6 @@ where Request::new(&uri) .method(Method::POST) - .header("Content-Length", &body.len()) .body(body) .send(writer) } @@ -787,6 +958,43 @@ mod tests { assert_eq!(&format!("{}", METHOD), "HEAD"); } + #[test] + fn authentication_basic() { + let auth = Authentication::basic("user", "password123"); + assert_eq!( + auth, + Authentication(AuthenticationType::Basic { + username: "user".to_string(), + password: "password123".to_string() + }) + ); + } + + #[test] + fn authentication_baerer() { + let auth = Authentication::bearer("456secret123token"); + assert_eq!( + auth, + Authentication(AuthenticationType::Bearer("456secret123token".to_string())) + ); + } + + #[test] + fn authentication_header() { + { + let auth = Authentication::basic("user", "password123"); + let (key, val) = auth.header(); + assert_eq!(key, "Authorization".to_string()); + assert_eq!(val, "Basic dXNlcjpwYXNzd29yZDEyMw==".to_string()); + } + { + let auth = Authentication::bearer("456secret123token"); + let (key, val) = auth.header(); + assert_eq!(key, "Authorization".to_string()); + assert_eq!(val, "Bearer 456secret123token".to_string()); + } + } + #[test] fn request_m_new() { RequestMessage::new(&Uri::try_from(URI).unwrap()); @@ -826,6 +1034,7 @@ mod tests { let mut expect_headers = Headers::new(); expect_headers.insert("Host", "doc.rust-lang.org"); + expect_headers.insert("User-Agent", "http_req/0.13.0"); expect_headers.insert(k, v); let req = req.header(k, v); @@ -833,6 +1042,24 @@ mod tests { assert_eq!(req.headers, expect_headers); } + #[test] + fn request_m_authentication() { + let uri = Uri::try_from(URI).unwrap(); + let mut req = RequestMessage::new(&uri); + let token = "456secret123token"; + let k = "Authorization"; + let v = "Bearer ".to_string() + token; + + let mut expect_headers = Headers::new(); + expect_headers.insert("Host", "doc.rust-lang.org"); + expect_headers.insert("User-Agent", "http_req/0.13.0"); + expect_headers.insert(k, &v); + + let req = req.authentication(Authentication::bearer(token)); + + assert_eq!(req.headers, expect_headers); + } + #[test] fn request_m_body() { let uri = Uri::try_from(URI).unwrap(); @@ -848,7 +1075,8 @@ mod tests { 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"; + Host: doc.rust-lang.org\r\n\ + User-Agent: http_req/0.13.0\r\n\r\n"; let msg = req.parse(); let msg = String::from_utf8_lossy(&msg).into_owned(); @@ -901,6 +1129,7 @@ mod tests { let mut expect_headers = Headers::new(); expect_headers.insert("Host", "doc.rust-lang.org"); expect_headers.insert("Connection", "Close"); + expect_headers.insert("User-Agent", "http_req/0.13.0"); expect_headers.insert(k, v); let req = req.header(k, v); diff --git a/src/response.rs b/src/response.rs index 68aff81..7a8e355 100644 --- a/src/response.rs +++ b/src/response.rs @@ -355,6 +355,24 @@ impl Headers { self.0.insert(Ascii::new(key.to_string()), val.to_string()) } + /// Inserts key-value pair into the headers and takes ownership over them. + /// + /// If the headers did not have this key present, None is returned. + /// + /// If the headers did have this key present, the value is updated, and the old value is returned. + /// The key is not updated, though; this matters for types that can be == without being identical. + /// + /// # Examples + /// ``` + /// use http_req::response::Headers; + /// + /// let mut headers = Headers::new(); + /// headers.insert_raw("Accept-Language".to_string(), "en-US".to_string()); + /// ``` + pub fn insert_raw(&mut self, key: String, val: String) -> Option { + self.0.insert(Ascii::new(key), val) + } + /// Creates default headers for a HTTP request /// /// # Examples @@ -366,8 +384,9 @@ impl Headers { /// let headers = Headers::default_http(&uri); /// ``` pub fn default_http(uri: &Uri) -> Headers { - let mut headers = Headers::with_capacity(4); + let mut headers = Headers::with_capacity(10); headers.insert("Host", &uri.host_header().unwrap_or_default()); + headers.insert("User-Agent", "http_req/0.13.0"); headers } @@ -826,6 +845,7 @@ mod tests { let mut headers = Headers::with_capacity(4); headers.insert("Host", "doc.rust-lang.org"); + headers.insert("User-Agent", "http_req/0.13.0"); assert_eq!(Headers::default_http(&uri), headers); } diff --git a/src/stream.rs b/src/stream.rs index e9840c6..4db1246 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -1,6 +1,10 @@ //! TCP stream - -use crate::{error::Error, tls, tls::Conn, uri::Uri, CR_LF, LF}; +use crate::{ + error::{Error, ParseErr}, + tls::{self, Conn}, + uri::Uri, + CR_LF, LF, +}; use std::{ io::{self, BufRead, Read, Write}, net::{TcpStream, ToSocketAddrs}, @@ -20,18 +24,12 @@ 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 host = match uri.host() { + Some(h) => h, + None => return Err(Error::Parse(ParseErr::UriErr)), + }; let port = uri.corr_port(); let stream = match connect_timeout { @@ -55,7 +53,10 @@ impl Stream { match stream { Stream::Http(http_stream) => { if uri.scheme() == "https" { - let host = uri.host().unwrap_or(""); + let host = match uri.host() { + Some(h) => h, + None => return Err(Error::Parse(ParseErr::UriErr)), + }; let mut cnf = tls::Config::default(); let cnf = match root_cert_file_pem { diff --git a/src/tls.rs b/src/tls.rs index edb5732..2357c8c 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -1,5 +1,4 @@ //! secure connection over TLS - use crate::error::Error as HttpError; use std::{ fs::File, diff --git a/src/uri.rs b/src/uri.rs index c4a65ce..6f55b6b 100644 --- a/src/uri.rs +++ b/src/uri.rs @@ -249,7 +249,7 @@ impl<'a> Uri<'a> { } /// Creates a new `Uri` from current uri and relative uri. - /// Transforms the relative uri into an absolute uri. + /// Writes the new uri (raw string) into `relative_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();