diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2ca91c8..2d68626 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -4,14 +4,15 @@ on: [push] jobs: build: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: Build - run: cargo build --verbose - - name: Build with Rusttls - run: cargo build --verbose --no-default-features --features rust-tls - - name: Run tests - run: cargo test --verbose + - uses: actions/checkout@v1 + - name: Build + run: cargo build --verbose + - name: Build with Rusttls + run: cargo build --verbose --no-default-features --features rust-tls + - name: Run tests + run: cargo test --verbose + - name: Run tests with rust-tls + run: cargo test --verbose --no-default-features --features rust-tls diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 30fe887..0935ec8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ # Contributing -Code and documentation PRs are always welcome. Just remember to list out introduced changes. +Code contributions and documentation improvements are always welcome! Please include a clear description of the changes introduced in your pull requests. -If you want to suggest some improvements, report a bug, discuss a functionality, etc., feel free to open an issue. +For suggestions, bug reports, or feature requests, please open an issue. diff --git a/Cargo.lock b/Cargo.lock index 2600154..d940a7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" @@ -23,9 +23,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.28.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f7720b74ed28ca77f90769a71fd8c637a0137f6fae4ae947e1050229cff57f" +checksum = "bfa9b6986f250236c27e5a204062434a773a13243d2ffc2955f37bdba4c5c6a1" dependencies = [ "bindgen", "cc", @@ -71,9 +71,9 @@ checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "cc" -version = "1.2.18" +version = "1.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" +checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0" dependencies = [ "jobserver", "libc", @@ -182,9 +182,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", @@ -220,7 +220,7 @@ dependencies = [ [[package]] name = "http_req" -version = "0.13.3" +version = "0.14.0" dependencies = [ "base64", "native-tls", @@ -266,9 +266,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.171" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libloading" @@ -288,9 +288,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "log" @@ -377,9 +377,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.107" +version = "0.9.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" dependencies = [ "cc", "libc", @@ -405,9 +405,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -464,7 +464,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -491,22 +491,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.5" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.9.3", + "linux-raw-sys 0.9.4", "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.23.25" +version = "0.23.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" dependencies = [ "aws-lc-rs", "log", @@ -590,9 +590,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -608,7 +608,7 @@ dependencies = [ "fastrand", "getrandom 0.3.2", "once_cell", - "rustix 1.0.5", + "rustix 1.0.7", "windows-sys 0.59.0", ] @@ -663,9 +663,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.8" +version = "0.26.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +checksum = "37493cadf42a2a939ed404698ded7fb378bf301b5011f973361779a3a74f8c93" dependencies = [ "rustls-pki-types", ] diff --git a/Cargo.toml b/Cargo.toml index 5186b84..d61fbfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http_req" -version = "0.13.3" +version = "0.14.0" license = "MIT" description = "simple and lightweight HTTP client with built-in HTTPS support" repository = "https://github.com/jayjamesjay/http_req" @@ -12,8 +12,8 @@ edition = "2021" [dependencies] unicase = "^2.8" -base64 = "^0.22.1" -zeroize = { version = "^1.8.1", features = ["zeroize_derive"] } +base64 = { version = "^0.22.1", optional = true } +zeroize = { version = "^1.8.1", features = ["zeroize_derive"], optional = true } native-tls = { version = "^0.2", optional = true } rustls = { version = "^0.23", optional = true } rustls-pemfile = { version = "^2.2", optional = true } @@ -22,11 +22,13 @@ webpki = { version = "^0.22", optional = true } webpki-roots = { version = "^0.26", optional = true } [features] -default = ["native-tls"] +default = ["native-tls", "auth"] rust-tls = [ "rustls", "rustls-pki-types", "webpki", "webpki-roots", "rustls-pemfile", + "auth", ] +auth = ["base64", "zeroize"] diff --git a/LICENSE b/LICENSE index 78d3280..8411ad3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018-2024 jayjamesjay +Copyright (c) 2018-2025 jayjamesjay Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 8188d55..1b98ce3 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,34 @@ # http_req [![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.13.3-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.3/http_req/) +[![Crates.io](https://img.shields.io/badge/crates.io-v0.14.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.14.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)) +- HTTP and HTTPS via [rust-native-tls](https://crates.io/crates/native-tls) (or optionally [rustls](https://crates.io/crates/rustls)) - Small binary size (0.7 MB for a basic GET request in the default configuratio) - Minimal number of dependencies ## Requirements -http_req by default uses [rust-native-tls](https://github.com/sfackler/rust-native-tls), +http_req by default uses [rust-native-tls](https://crates.io/crates/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; - -fn main() { - let mut body = Vec::new(); //Container for body of a response. - let res = request::get("https://doc.rust-lang.org/", &mut body).unwrap(); - - println!("Status: {} {}", res.status_code(), res.reason()); -} -``` - -Take a look at [more examples](https://github.com/jayjamesjay/http_req/tree/master/examples) +on all other platforms. But it also supports [rustls](https://crates.io/crates/rustls). + +## All functionalities + +- Support for both HTTP and HTTPS protocols via [rust-native-tls](https://crates.io/crates/native-tls) (or optionally [rustls](https://crates.io/crates/rustls)) +- Creating and sending HTTP requests using the `Request` type (with extended capabilities provided via `RequestMessage` and `Stream`) +- Representing HTTP responses with the `Response` type, allowing easy access to details like the status code and headers +- Handling redirects using the `RedirectPolicy` +- Support for Basic and Bearer authentication +- Processing responses with `Transfer-Encoding: chunked` +- Managing absolute `Uri`s and partial support for relative `Uri`s +- Enforcing timeouts on requests +- Downloading data in a streaming fashion, allowing direct saving to disk (minimizing RAM usage) +- `Error` handling system allowing for better debugging +- Utility functions for easily sending common request types: `get`, `head`, `post` ## Usage @@ -41,7 +38,7 @@ In order to use `http_req` with default configuration, add the following lines t ```toml [dependencies] -http_req = "^0.13" +http_req = "^0.14" ``` ### Rustls @@ -50,9 +47,35 @@ In order to use `http_req` with `rustls` in your project, add the following line ```toml [dependencies] -http_req = { version="^0.13", default-features = false, features = ["rust-tls"] } +http_req = { version="^0.14", default-features = false, features = ["rust-tls"] } +``` + +### HTTP only + +In order to use `http_req` without any additional features in your project (no HTTPS, no Authentication), add the following lines to `Cargo.toml`: + +```toml +[dependencies] +http_req = { version="^0.14", default-features = false } +``` + +## Example + +Basic HTTP GET request + +```rust +use http_req::request; + +fn main() { + let mut body = Vec::new(); //Container for body of a response. + let res = request::get("https://doc.rust-lang.org/", &mut body).unwrap(); + + println!("Status: {} {}", res.status_code(), res.reason()); +} ``` +Take a look at [more examples](https://github.com/jayjamesjay/http_req/tree/master/examples) + ## License Licensed under [MIT](https://github.com/jayjamesjay/http_req/blob/master/LICENSE). diff --git a/examples/authentication.rs b/examples/authentication.rs index 5bdffe5..c67328b 100644 --- a/examples/authentication.rs +++ b/examples/authentication.rs @@ -7,7 +7,7 @@ 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(); + let uri = Uri::try_from("https://httpbin.org/basic-auth/foo/bar").unwrap(); // Authentication details: username and password. let auth = Authentication::basic("foo", "bar"); diff --git a/src/chunked.rs b/src/chunked.rs index 25ac72d..d23a200 100644 --- a/src/chunked.rs +++ b/src/chunked.rs @@ -1,6 +1,10 @@ //! support for Transfer-Encoding: chunked + use crate::CR_LF; -use std::io::{self, BufRead, BufReader, Error, ErrorKind, Read}; +use std::{ + cmp, + io::{self, BufRead, BufReader, Error, ErrorKind, Read}, +}; const MAX_LINE_LENGTH: usize = 4096; @@ -20,9 +24,8 @@ where R: Read, { fn read(&mut self, buf: &mut [u8]) -> io::Result { - // the length of data already read out - let mut consumed = 0usize; - let mut footer = [0u8; 2]; + let mut consumed = 0; + let mut footer: [u8; 2] = [0; 2]; while !self.eof && self.err.is_none() { if self.check_end { @@ -33,25 +36,23 @@ where break; } - if let Ok(_) = self.reader.read_exact(&mut footer) { - if &footer != CR_LF { - self.err = Some(error_malformed_chunked_encoding()); - break; - } + if self.reader.read_exact(&mut footer).is_ok() && &footer != CR_LF { + self.err = Some(Error::new( + ErrorKind::InvalidData, + "Malformed chunked encoding", + )); + break; } self.check_end = false; } if self.n == 0 { - if consumed > 0 && !self.chunk_header_avaliable() { - // We've read enough. Don't potentially block - // reading a new chunk header. + if consumed > 0 && !self.chunk_header_available() { break; } self.begin_chunk(); - continue; } @@ -59,13 +60,9 @@ where break; } - let end = if consumed + self.n < buf.len() { - consumed + self.n - } else { - buf.len() - }; + let end = cmp::min(consumed + self.n, buf.len()); - let mut n0 = 0usize; + let mut n0: usize = 0; match self.reader.read(&mut buf[consumed..end]) { Ok(v) => n0 = v, Err(err) => self.err = Some(err), @@ -82,10 +79,7 @@ where } match self.err.as_ref() { - Some(v) => Err(Error::new( - v.kind(), - format!("wrapper by chunked: {}", v.to_string()), - )), + Some(v) => Err(Error::new(v.kind(), v.to_string())), None => Ok(consumed), } } @@ -137,8 +131,11 @@ where } } + /// Begins a new chunk by reading and parsing its header. + /// + /// This function reads one full line representing the size of the next HTTP/1.x chunk. + /// If there is an error during this process (such as malformed data), it records that error for later use. fn begin_chunk(&mut self) { - // chunk-size CRLF let line = match read_chunk_line(&mut self.reader) { Ok(v) => v, Err(err) => { @@ -149,25 +146,26 @@ where match parse_hex_uint(line) { Ok(v) => self.n = v, - Err(err) => self.err = Some(Error::new(ErrorKind::Other, err)), + Err(err) => self.err = Some(Error::new(ErrorKind::InvalidData, err)), } self.eof = self.n == 0; } - fn chunk_header_avaliable(&self) -> bool { - self.reader.buffer().iter().find(|&&c| c == b'\n').is_some() + /// Checks whether a chunk header is available. + fn chunk_header_available(&self) -> bool { + self.reader.buffer().iter().any(|&c| c == b'\n') } } -fn error_line_too_long() -> Error { - Error::new(ErrorKind::Other, "header line too long") -} - -fn error_malformed_chunked_encoding() -> Error { - Error::new(ErrorKind::Other, "malformed chunked encoding") -} - +/// Checks if a given byte is an ASCII space character. +/// +/// This function checks whether a single byte, b, +/// represents one of the following characters: +/// - Space (ASCII 0x20) +/// - Tab (ASCII 0x09) +/// - Line Feed (ASCII 0xA) or Carriage Return (ASCII 0xD), which are used to move +/// positions in text. These two together indicate an end of line. fn is_ascii_space(b: u8) -> bool { match b { b' ' | b'\t' | b'\n' | b'\r' => true, @@ -175,18 +173,20 @@ fn is_ascii_space(b: u8) -> bool { } } +/// Parses an integer represented by hexadecimal digits from bytes. fn parse_hex_uint<'a>(data: Vec) -> Result { - let mut n = 0usize; + let mut n = 0; + for (i, v) in data.iter().enumerate() { if i == 16 { - return Err("http chunk length too large"); + return Err("HTTP chunk length is too large"); } let vv = match *v { b'0'..=b'9' => v - b'0', b'a'..=b'f' => v - b'a' + 10, b'A'..=b'F' => v - b'A' + 10, - _ => return Err("invalid byte in chunk length"), + _ => return Err("Invalid byte in chunk length"), }; n <<= 4; @@ -196,6 +196,7 @@ fn parse_hex_uint<'a>(data: Vec) -> Result { Ok(n) } +/// Reads a single chunk line from `BufReader`. fn read_chunk_line(b: &mut BufReader) -> io::Result> where R: Read, @@ -204,7 +205,10 @@ where b.read_until(b'\n', &mut line)?; if line.len() > MAX_LINE_LENGTH { - return Err(error_line_too_long()); + return Err(Error::new( + ErrorKind::InvalidData, + "Exceeded maximum line length", + )); } trim_trailing_whitespace(&mut line); @@ -213,32 +217,33 @@ where Ok(line) } +/// Removes any trailing chunk extensions from a vector containing bytes (`Vec`). fn remove_chunk_extension(v: &mut Vec) { - if let Some(idx) = v.iter().position(|v| *v == b';') { - v.resize(idx, 0); + if let Some(idx) = v.iter().position(|&v| v == b';') { + v.truncate(idx); } } +/// Remove any trailing whitespace characters (specifically ASCII spaces) +/// from the end of a vector containing bytes (`Vec`). fn trim_trailing_whitespace(v: &mut Vec) { - if v.len() == 0 { + if v.is_empty() { return; } - for i in (0..(v.len() - 1)).rev() { - if !is_ascii_space(v[i]) { - v.resize(i + 1, 0); - return; + while let Some(&last_byte) = v.last() { + if !is_ascii_space(last_byte) { + break; } - } - v.clear(); + v.pop(); + } } #[cfg(test)] mod tests { - use std::io::{self, Read}; - use super::*; + use std::io::{self, Read}; #[test] fn read() { @@ -249,6 +254,7 @@ mod tests { assert_eq!("hello, world! 0123456789abcdef".as_bytes(), &writer[..]); } + #[test] fn read_multiple() { { @@ -270,6 +276,7 @@ mod tests { assert_eq!("foo".as_bytes(), &writer[..]); } } + #[test] fn read_partial() { let data: &[u8] = b"7\r\n1234567"; @@ -279,6 +286,7 @@ mod tests { assert_eq!("1234567".as_bytes(), &writer[..]); } + #[test] fn read_ignore_extensions() { let data_str = String::from("7;ext=\"some quoted string\"\r\n") diff --git a/src/error.rs b/src/error.rs index 4e54570..5f80e38 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,14 +1,30 @@ //! error system used around the library. + use std::{error, fmt, io, num, str, sync::mpsc}; +/// Enum representing different parsing errors encountered by the library. #[derive(Debug, PartialEq)] pub enum ParseErr { + /// Error related to invalid UTF-8 character sequences encountered + /// during string processing or conversion operations. Utf8(str::Utf8Error), + + /// Failure in parsing integer values from strings using standard + /// number formats, such as those conforming to base 10 conventions. Int(num::ParseIntError), + + /// Issue encountered when processing status line from HTTP response message. StatusErr, + + /// Issue encountered when processing headers from HTTP response message. HeadersErr, + + /// Issue arising while processing URIs that contain invalid + /// characters or do not follow the URI specification. UriErr, - Invalid, + + /// Error indicating that provided string, vector, or other element + /// does not contain any values that could be parsed. Empty, } @@ -19,7 +35,7 @@ impl error::Error for ParseErr { match self { Utf8(e) => Some(e), Int(e) => Some(e), - StatusErr | HeadersErr | UriErr | Invalid | Empty => None, + _ => None, } } } @@ -29,13 +45,12 @@ impl fmt::Display for ParseErr { use self::ParseErr::*; let err = match self { - Utf8(_) => "Invalid character", + Utf8(_) => "Invalid character sequence", 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", + Empty => "Nothing to parse", }; write!(f, "ParseErr: {}", err) } @@ -53,12 +68,26 @@ impl From for ParseErr { } } +/// Enum representing various errors encountered by the library. #[derive(Debug)] pub enum Error { + /// IO error that occurred during file operations, + /// network connections, or any other type of I/O operation. IO(io::Error), + + /// Error encountered while parsing data using the library's functions. Parse(ParseErr), + + /// Timeout error, indicating that an operation timed out + /// after waiting for the specified duration. Timeout, + + /// Error encountered while using TLS/SSL cryptographic protocols, + /// such as establishing secure connections with servers. Tls, + + /// Thread-related communication error, signifying an issue + /// that occurred during inter-thread communication. Thread, } @@ -69,7 +98,7 @@ impl error::Error for Error { match self { IO(e) => Some(e), Parse(e) => Some(e), - Timeout | Tls | Thread => None, + _ => None, } } } @@ -79,8 +108,8 @@ impl fmt::Display for Error { use self::Error::*; let err = match self { - IO(_) => "IO error", - Parse(err) => return err.fmt(f), + IO(e) => &format!("IO Error - {}", e), + Parse(e) => return e.fmt(f), Timeout => "Timeout error", Tls => "TLS error", Thread => "Thread communication error", diff --git a/src/lib.rs b/src/lib.rs index 77ad98e..d1b3950 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,24 +4,26 @@ //! 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 +//! ## Examples //! Basic GET request //! ``` //! use http_req::request; //! //! fn main() { -//! //Container for body of a response +//! // Container for body of a response //! let mut body = Vec::new(); //! let res = request::get("https://doc.rust-lang.org/", &mut body).unwrap(); //! //! println!("Status: {} {}", res.status_code(), res.reason()); //! } //! ``` + pub mod chunked; pub mod error; pub mod request; pub mod response; pub mod stream; +#[cfg(any(feature = "native-tls", feature = "rust-tls"))] pub mod tls; pub mod uri; diff --git a/src/request.rs b/src/request.rs index 1fef638..5f07af3 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,4 +1,5 @@ //! creating and sending HTTP requests + use crate::{ chunked::ChunkReader, error, @@ -6,7 +7,8 @@ use crate::{ stream::{Stream, ThreadReceive, ThreadSend}, uri::Uri, }; -use base64::engine::{general_purpose::URL_SAFE, Engine}; +#[cfg(feature = "auth")] +use base64::prelude::*; use std::{ convert::TryFrom, fmt, @@ -16,6 +18,7 @@ use std::{ thread, time::{Duration, Instant}, }; +#[cfg(feature = "auth")] use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; const CR_LF: &str = "\r\n"; @@ -38,7 +41,7 @@ pub enum Method { } impl Method { - /// Returns a string representation of a HTTP request method. + /// Returns a string representation of an HTTP request method. /// /// # Examples /// ``` @@ -79,7 +82,7 @@ pub enum HttpVersion { } impl HttpVersion { - /// Returns a string representation of a HTTP version. + /// Returns a string representation of an HTTP version. /// /// # Examples /// ``` @@ -108,9 +111,11 @@ impl fmt::Display for HttpVersion { /// Authentication details: /// - Basic: username and password /// - Bearer: token +#[cfg(feature = "auth")] #[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop)] pub struct Authentication(AuthenticationType); +#[cfg(feature = "auth")] impl Authentication { /// Creates a new `Authentication` of type `Basic`. /// @@ -146,7 +151,8 @@ impl Authentication { Authentication(AuthenticationType::Bearer(token.to_string())) } - /// Generates a HTTP Authorization header. Returns `key` & `value` pair. + /// Generates an HTTP Authorization header. Returns a `key` & `value` pair. + /// /// - Basic: uses base64 encoding on provided credentials /// - Bearer: uses token as is /// @@ -170,13 +176,15 @@ impl Authentication { /// Authentication types #[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop)] +#[cfg(feature = "auth")] enum AuthenticationType { Basic { username: String, password: String }, Bearer(String), } +#[cfg(feature = "auth")] impl AuthenticationType { - /// Returns scheme + /// Returns the authentication scheme as a string. const fn scheme(&self) -> &str { use AuthenticationType::*; @@ -195,20 +203,20 @@ impl AuthenticationType { match self { Basic { username, password } => { - let credentials = Zeroizing::new(username.to_string() + ":" + password); - Zeroizing::new(URL_SAFE.encode(credentials.as_bytes())) + let credentials = Zeroizing::new(format!("{}:{}", username, password)); + Zeroizing::new(BASE64_STANDARD.encode(credentials.as_bytes())) } Bearer(token) => Zeroizing::new(token.to_string()), } } } -/// Allows to control redirects +/// Allows control over 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. + /// Runs a function `F` to determine if the redirect should be followed. Custom(F), } @@ -216,9 +224,9 @@ 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 + /// Evaluates the policy against specified conditions: + /// - `Limit`: Checks if limit is greater than 0 and decrements it by one each time a redirect is followed. + /// - `Custom`: Executes function `F` with the URI, returning its result to decide on following the redirect. /// /// # Examples /// ``` @@ -226,12 +234,18 @@ where /// /// let uri: &str = "https://www.rust-lang.org/learn"; /// + /// // Follows redirects up to 5 times as per `Limit` policy. /// let mut policy_1: RedirectPolicy bool> = RedirectPolicy::Limit(5); - /// assert_eq!(policy_1.follow(&uri), true); + /// assert_eq!(policy_1.follow(&uri), true); // First call, limit is 5 /// - /// let mut policy_2: RedirectPolicy bool> = RedirectPolicy::Custom(|uri| false); + /// // Does not follow redirects due to zero `Limit`. + /// let mut policy_2: RedirectPolicy bool> = RedirectPolicy::Limit(0); /// assert_eq!(policy_2.follow(&uri), false); - /// ``` + /// + /// // Custom policy returning false, hence no redirect. + /// let mut policy_3: RedirectPolicy bool> = RedirectPolicy::Custom(|_| false); + /// assert_eq!(policy_3.follow(&uri), false); + ///``` pub fn follow(&mut self, uri: &str) -> bool { use self::RedirectPolicy::*; @@ -257,7 +271,7 @@ where } } -/// Raw HTTP request message that can be sent to any stream +/// Raw HTTP request message that can be sent to any stream. /// /// # Examples /// ``` @@ -267,8 +281,8 @@ where /// let addr: Uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// /// let mut request_msg = RequestMessage::new(&addr) -/// .header("Connection", "Close") -/// .parse(); +/// .header("Connection", "Close") +/// .parse(); /// ``` #[derive(Clone, Debug, PartialEq)] pub struct RequestMessage<'a> { @@ -280,7 +294,7 @@ pub struct RequestMessage<'a> { } impl<'a> RequestMessage<'a> { - /// Creates a new `RequestMessage` with default parameters + /// Creates a new `RequestMessage` with default parameters. /// /// # Examples /// ``` @@ -302,7 +316,7 @@ impl<'a> RequestMessage<'a> { } } - /// Sets the request method + /// Sets the request method. /// /// # Examples /// ``` @@ -322,7 +336,7 @@ impl<'a> RequestMessage<'a> { self } - /// Sets the HTTP version + /// Sets the HTTP version. /// /// # Examples /// ``` @@ -342,7 +356,7 @@ impl<'a> RequestMessage<'a> { self } - /// Replaces all it's headers with headers passed to the function + /// Replaces all its headers with the provided headers. /// /// # Examples /// ``` @@ -368,7 +382,7 @@ impl<'a> RequestMessage<'a> { self } - /// Adds a new header to existing/default headers + /// Adds a new header to the existing/default headers. /// /// # Examples /// ``` @@ -389,7 +403,7 @@ impl<'a> RequestMessage<'a> { self } - /// Adds an authorization header to existing headers + /// Adds an authorization header to existing headers. /// /// # Examples /// ``` @@ -401,6 +415,7 @@ impl<'a> RequestMessage<'a> { /// let request_msg = RequestMessage::new(&addr) /// .authentication(Authentication::bearer("secret456token123")); /// ``` + #[cfg(feature = "auth")] pub fn authentication(&mut self, auth: T) -> &mut Self where Authentication: From, @@ -412,7 +427,7 @@ impl<'a> RequestMessage<'a> { self } - /// Sets the body for request + /// Sets the body for the request. /// /// # Examples /// ``` @@ -432,7 +447,7 @@ impl<'a> RequestMessage<'a> { self } - /// Parses the request message for this `RequestMessage` + /// Parses the request message for this `RequestMessage`. /// /// # Examples /// ``` @@ -442,8 +457,8 @@ impl<'a> RequestMessage<'a> { /// let addr: Uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// /// let mut request_msg = RequestMessage::new(&addr) - /// .header("Connection", "Close") - /// .parse(); + /// .header("Connection", "Close") + /// .parse(); /// ``` pub fn parse(&self) -> Vec { let mut request_msg = format!( @@ -469,8 +484,8 @@ impl<'a> RequestMessage<'a> { /// Allows for making HTTP requests based on specified parameters. /// -/// It creates a stream (`TcpStream` or `TlsStream`) appropriate for the type of uri (`http`/`https`). -/// By default it closes connection after completion of the response. +/// This implementation creates a stream (`TcpStream` or `TlsStream`) appropriate for the URI type (`http`/`https`). +/// By default, it closes the connection after completing the response. /// /// # Examples /// ``` @@ -480,13 +495,13 @@ impl<'a> RequestMessage<'a> { /// let mut writer = Vec::new(); /// let uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// -/// let response = Request::new(&uri).send(&mut writer).unwrap();; +/// let response = Request::new(&uri).send(&mut writer).unwrap(); /// assert_eq!(response.status_code(), StatusCode::new(200)); /// ``` /// #[derive(Clone, Debug, PartialEq)] pub struct Request<'a> { - messsage: RequestMessage<'a>, + message: RequestMessage<'a>, redirect_policy: RedirectPolicy bool>, connect_timeout: Option, read_timeout: Option, @@ -496,7 +511,7 @@ pub struct Request<'a> { } impl<'a> Request<'a> { - /// Creates a new `Request` with default parameters. + /// Creates a new `Request`. Initializes the request with default values and sets the "Connection" header to "Close". /// /// # Examples /// ``` @@ -512,7 +527,7 @@ impl<'a> Request<'a> { message.header("Connection", "Close"); Request { - messsage: message, + message, redirect_policy: RedirectPolicy::default(), connect_timeout: Some(Duration::from_secs(DEFAULT_CALL_TIMEOUT)), read_timeout: Some(Duration::from_secs(DEFAULT_CALL_TIMEOUT)), @@ -538,7 +553,7 @@ impl<'a> Request<'a> { where Method: From, { - self.messsage.method(method); + self.message.method(method); self } @@ -554,20 +569,19 @@ impl<'a> Request<'a> { /// let request = Request::new(&uri) /// .version(HttpVersion::Http10); /// ``` - pub fn version(&mut self, version: T) -> &mut Self where HttpVersion: From, { - self.messsage.version(version); + self.message.version(version); self } - /// Replaces all it's headers with headers passed to the function. + /// Replaces all its headers with the provided headers. /// /// # Examples /// ``` - /// use http_req::{request::Request, uri::Uri, response::Headers}; + /// use http_req::{request::Request, response::Headers, uri::Uri}; /// use std::convert::TryFrom; /// /// let uri: Uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); @@ -585,11 +599,11 @@ impl<'a> Request<'a> { where Headers: From, { - self.messsage.headers(headers); + self.message.headers(headers); self } - /// Adds the header to existing/default headers. + /// Adds a new header to the existing/default headers. /// /// # Examples /// ``` @@ -606,7 +620,7 @@ impl<'a> Request<'a> { T: ToString + ?Sized, U: ToString + ?Sized, { - self.messsage.header(key, val); + self.message.header(key, val); self } @@ -615,22 +629,23 @@ impl<'a> Request<'a> { /// # Examples /// ``` /// use std::convert::TryFrom; - /// use http_req::{request::{RequestMessage, Authentication}, response::Headers, uri::Uri}; + /// use http_req::{request::{Request, Authentication}, uri::Uri}; /// /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - /// let request_msg = RequestMessage::new(&addr) + /// let request = Request::new(&addr) /// .authentication(Authentication::bearer("secret456token123")); /// ``` + #[cfg(feature = "auth")] pub fn authentication(&mut self, auth: T) -> &mut Self where Authentication: From, { - self.messsage.authentication(auth); + self.message.authentication(auth); self } - /// Sets the body for request. + /// Sets the body for the request. /// /// # Examples /// ``` @@ -646,7 +661,7 @@ impl<'a> Request<'a> { /// .body(body); /// ``` pub fn body(&mut self, body: &'a [u8]) -> &mut Self { - self.messsage.body(body); + self.message.body(body); self } @@ -732,7 +747,8 @@ impl<'a> Request<'a> { self } - /// Sets the timeout on entire request. + /// Sets the timeout for the entire request. + /// /// Data is read from a stream until there is no more data to read or the timeout is exceeded. /// /// # Examples @@ -754,7 +770,7 @@ impl<'a> Request<'a> { self } - /// Adds the file containing the PEM-encoded certificates that should be added in the trusted root store. + /// Adds the file containing the PEM-encoded certificates that should be added to the trusted root store. /// /// # Examples /// ``` @@ -794,8 +810,9 @@ impl<'a> Request<'a> { /// Sends the HTTP request and returns `Response`. /// - /// Creates `TcpStream` (and wraps it with `TlsStream` if needed). Writes request message - /// to created stream. Returns response for this request. Writes response's body to `writer`. + /// This method sets up a stream, writes the request message to it, and processes the response. + /// The connection is closed after processing. If the response indicates a redirect and the policy allows, + /// a new request is sent following the redirection. /// /// # Examples /// ``` @@ -812,13 +829,17 @@ impl<'a> Request<'a> { T: Write, { // Set up a stream. - let mut stream = Stream::connect(self.messsage.uri, self.connect_timeout)?; + let mut stream = Stream::connect(self.message.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.messsage.uri, self.root_cert_file_pem)?; - // Send the request message to stream. - let request_msg = self.messsage.parse(); + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] + { + stream = Stream::try_to_https(stream, self.message.uri, self.root_cert_file_pem)?; + } + + // Send the request message to the stream. + let request_msg = self.message.parse(); stream.write_all(&request_msg)?; // Set up variables @@ -833,7 +854,7 @@ impl<'a> Request<'a> { buf_reader.send_head(&sender); let params: Vec<&str> = receiver_supp.recv().unwrap_or(Vec::new()); - if params.contains(&"non-empty") { + if !params.is_empty() && params.contains(&"non-empty") { if params.contains(&"chunked") { let mut buf_reader = ChunkReader::from(buf_reader); buf_reader.send_all(&sender); @@ -852,7 +873,7 @@ impl<'a> Request<'a> { 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) + self.message.uri.from_relative(&mut raw_uri) } else { Uri::try_from(raw_uri.as_str()) }?; @@ -864,7 +885,7 @@ impl<'a> Request<'a> { } } - let params = response.basic_info(&self.messsage.method).to_vec(); + let params = response.basic_info(&self.message.method).to_vec(); sender_supp.send(params)?; // Receive and process `body` of the response. @@ -877,7 +898,7 @@ impl<'a> Request<'a> { } } -/// Creates and sends GET request. Returns response for this request. +/// Creates and sends a GET request. Returns the response for this request. /// /// # Examples /// ``` @@ -897,7 +918,7 @@ where Request::new(&uri).send(writer) } -/// Creates and sends HEAD request. Returns response for this request. +/// Creates and sends a HEAD request. Returns the response for this request. /// /// # Examples /// ``` @@ -916,7 +937,7 @@ where Request::new(&uri).method(Method::HEAD).send(&mut writer) } -/// Creates and sends POST request. Returns response for this request. +/// Creates and sends a POST request. Returns the response for this request. /// /// # Examples /// ``` @@ -959,6 +980,7 @@ mod tests { } #[test] + #[cfg(feature = "auth")] fn authentication_basic() { let auth = Authentication::basic("user", "password123"); assert_eq!( @@ -971,6 +993,7 @@ mod tests { } #[test] + #[cfg(feature = "auth")] fn authentication_baerer() { let auth = Authentication::bearer("456secret123token"); assert_eq!( @@ -980,6 +1003,7 @@ mod tests { } #[test] + #[cfg(feature = "auth")] fn authentication_header() { { let auth = Authentication::basic("user", "password123"); @@ -1043,6 +1067,7 @@ mod tests { } #[test] + #[cfg(feature = "auth")] fn request_m_authentication() { let uri = Uri::try_from(URI).unwrap(); let mut req = RequestMessage::new(&uri); @@ -1101,7 +1126,7 @@ mod tests { let mut req = Request::new(&uri); req.method(Method::HEAD); - assert_eq!(req.messsage.method, Method::HEAD); + assert_eq!(req.message.method, Method::HEAD); } #[test] @@ -1116,7 +1141,7 @@ mod tests { let mut req = Request::new(&uri); let req = req.headers(headers.clone()); - assert_eq!(req.messsage.headers, headers); + assert_eq!(req.message.headers, headers); } #[test] @@ -1134,7 +1159,7 @@ mod tests { let req = req.header(k, v); - assert_eq!(req.messsage.headers, expect_headers); + assert_eq!(req.message.headers, expect_headers); } #[test] @@ -1143,7 +1168,7 @@ mod tests { let mut req = Request::new(&uri); let req = req.body(&BODY); - assert_eq!(req.messsage.body, Some(BODY.as_ref())); + assert_eq!(req.message.body, Some(BODY.as_ref())); } #[test] diff --git a/src/response.rs b/src/response.rs index 7a8e355..69aed57 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,4 +1,5 @@ //! parsing server response + use crate::{ error::{Error, ParseErr}, request::Method, @@ -24,7 +25,7 @@ pub struct Response { } impl Response { - /// Creates new `Response` with head - status and headers - parsed from a slice of bytes + /// Creates a new `Response` with head - status and headers - parsed from a slice of bytes. /// /// # Examples /// ``` @@ -46,7 +47,7 @@ impl Response { Ok(Response { status, headers }) } - /// Parses `Response` from slice of bytes. Writes it's body to `writer`. + /// Parses `Response` from slice of bytes. Writes its body to `writer`. /// /// # Examples /// ``` @@ -66,18 +67,23 @@ impl Response { T: Write, { if res.is_empty() { - Err(Error::Parse(ParseErr::Empty)) - } else { - let pos = match find_slice(res, &CR_LF_2) { - Some(v) => v, - None => res.len(), - }; + return Err(Error::Parse(ParseErr::Empty)); + } - let response = Self::from_head(&res[..pos])?; - writer.write_all(&res[pos..])?; + let pos = match find_slice(res, &CR_LF_2) { + Some(v) => v, + None => res.len(), + }; + + // Attempt to parse the headers and status from the response + let response = Self::from_head(&res[..pos])?; - Ok(response) + // Write any remaining part of the bytes (assumed body) into writer + if pos < res.len() { + writer.write_all(&res[pos..])?; } + + Ok(response) } /// Returns status code of this `Response`. @@ -160,8 +166,8 @@ impl Response { &self.headers } - /// Returns length of the content of this `Response` as a `Option`, according to information - /// included in headers. If there is no such an information, returns `None`. + /// Returns length of the content of this `Response` as an `Option`, according to information + /// included in headers. If there is no such information, returns `None`. /// /// # Examples /// ``` @@ -184,6 +190,22 @@ impl Response { } /// Checks if Transfer-Encoding includes "chunked". + /// + /// # Examples + /// ``` + /// use http_req::response::Response; + /// + /// const RESPONSE: &[u8; 157] = b"HTTP/1.1 200 OK\r\n\ + /// Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ + /// Content-Type: text/html\r\n\ + /// Transfer-Encoding: chunked\r\n\ + /// Content-Length: 100\r\n\r\n\ + /// hello\r\n\r\nhello"; + /// let mut body = Vec::new(); + /// + /// let response = Response::try_from(RESPONSE, &mut body).unwrap(); + /// assert!(response.is_chunked()); + /// ``` pub fn is_chunked(&self) -> bool { self.headers() .get("Transfer-Encoding") @@ -209,7 +231,12 @@ impl Response { } } -/// Status of HTTP response +/// Represents the status line of an HTTP response. +/// +/// The `Status` encapsulates 3 components: +/// - `version`: an HTTP version (e.g. "HTTP/1.1"). +/// - `code`: a status code (e.g. 200 for OK). +/// - `reason`: a reason phrase associated with the status code (e.g. "OK"). #[derive(PartialEq, Debug, Clone)] pub struct Status { version: String, @@ -218,6 +245,7 @@ pub struct Status { } impl Status { + /// Creates a new `Status` from a version, a code, and a reason. pub fn new(version: &str, code: StatusCode, reason: &str) -> Status { Status::from((version, code, reason)) } @@ -242,11 +270,13 @@ impl str::FromStr for Status { type Err = ParseErr; fn from_str(status_line: &str) -> Result { - let mut status_line = status_line.trim().splitn(3, ' '); + let mut parts = status_line.trim().splitn(3, ' '); + + let version = parts.next().ok_or(ParseErr::StatusErr)?; + let code: StatusCode = parts.next().ok_or(ParseErr::StatusErr)?.parse()?; - let version = status_line.next().ok_or(ParseErr::StatusErr)?; - let code: StatusCode = status_line.next().ok_or(ParseErr::StatusErr)?.parse()?; - let reason = match status_line.next() { + // Check if the reason phrase is provided + let reason = match parts.next() { Some(reason) => reason, None => code.reason().unwrap_or("Unknown"), }; @@ -255,9 +285,9 @@ impl str::FromStr for Status { } } -/// Wrapper around `HashMap, String>` with additional functionality for parsing HTTP headers +/// Wrapper around `HashMap, String>` with additional functionality for parsing HTTP headers. /// -/// # Example +/// # Examples /// ``` /// use http_req::response::Headers; /// @@ -398,12 +428,17 @@ impl str::FromStr for Headers { fn from_str(s: &str) -> Result { let headers = s.trim(); + if headers.is_empty() { + return Err(ParseErr::HeadersErr); + } + if headers.lines().all(|e| e.contains(':')) { let headers = headers .lines() .map(|elem| { let idx = elem.find(':').unwrap(); let (key, value) = elem.split_at(idx); + (Ascii::new(key.to_string()), value[1..].trim().to_string()) }) .collect(); @@ -438,9 +473,12 @@ impl fmt::Display for Headers { } } -/// Code sent by a server in response to a client's request. +/// Represents an HTTP status code. /// -/// # Example +/// An HTTP status code is a three-digit number sent by a server +/// in response to a client's request. +/// +/// # Examples /// ``` /// use http_req::response::StatusCode; /// @@ -451,7 +489,7 @@ impl fmt::Display for Headers { pub struct StatusCode(u16); impl StatusCode { - /// Creates new StatusCode from `u16` value. + /// Creates a new `StatusCode` from `u16` value. /// /// # Examples /// ``` @@ -671,14 +709,20 @@ mod tests { use std::convert::TryFrom; const RESPONSE: &[u8; 129] = b"HTTP/1.1 200 OK\r\n\ - Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ - Content-Type: text/html\r\n\ - Content-Length: 100\r\n\r\n\ - hello\r\n\r\nhello"; + Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ + Content-Type: text/html\r\n\ + Content-Length: 100\r\n\r\n\ + hello\r\n\r\nhello"; const RESPONSE_H: &[u8; 102] = b"HTTP/1.1 200 OK\r\n\ - Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ - Content-Type: text/html\r\n\ - Content-Length: 100\r\n\r\n"; + Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ + Content-Type: text/html\r\n\ + Content-Length: 100\r\n\r\n"; + const RESPONSE_C: &[u8; 157] = b"HTTP/1.1 200 OK\r\n\ + Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ + Content-Type: text/html\r\n\ + Transfer-Encoding: chunked\r\n\ + Content-Length: 100\r\n\r\n\ + hello\r\n\r\nhello"; const BODY: &[u8; 27] = b"hello\r\n\r\nhello"; const STATUS_LINE: &str = "HTTP/1.1 200 OK"; @@ -1003,6 +1047,41 @@ mod tests { assert_eq!(res.content_len(), Some(100)); } + #[test] + fn res_is_chunked() { + { + let mut writer = Vec::with_capacity(101); + let res = Response::try_from(RESPONSE, &mut writer).unwrap(); + + assert!(!res.is_chunked()); + } + { + let mut writer = Vec::with_capacity(101); + let res = Response::try_from(RESPONSE_C, &mut writer).unwrap(); + + assert!(res.is_chunked()); + } + } + + #[test] + fn res_basic_info() { + { + let mut writer = Vec::with_capacity(101); + let res = Response::try_from(RESPONSE, &mut writer).unwrap(); + let basic_info = res.basic_info(&Method::GET); + + assert!(basic_info.contains(&"non-empty")); + } + { + let mut writer = Vec::with_capacity(101); + let res = Response::try_from(RESPONSE_C, &mut writer).unwrap(); + let basic_info = res.basic_info(&Method::GET); + + assert!(basic_info.contains(&"non-empty")); + assert!(basic_info.contains(&"chunked")); + } + } + #[test] fn res_body() { { diff --git a/src/stream.rs b/src/stream.rs index 4db1246..851bf6b 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -1,14 +1,17 @@ //! TCP stream + +#[cfg(any(feature = "native-tls", feature = "rust-tls"))] +use crate::tls::{self, Conn}; use crate::{ error::{Error, ParseErr}, - tls::{self, Conn}, uri::Uri, CR_LF, LF, }; +#[cfg(any(feature = "native-tls", feature = "rust-tls"))] +use std::path::Path; use std::{ io::{self, BufRead, Read, Write}, net::{TcpStream, ToSocketAddrs}, - path::Path, sync::mpsc::{Receiver, RecvTimeoutError, Sender}, time::{Duration, Instant}, }; @@ -20,16 +23,14 @@ const BUF_SIZE: usize = 16 * 1000; #[derive(Debug)] pub enum Stream { Http(TcpStream), + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] Https(Conn), } impl Stream { /// 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 = match uri.host() { - Some(h) => h, - None => return Err(Error::Parse(ParseErr::UriErr)), - }; + let host = uri.host().ok_or(Error::Parse(ParseErr::UriErr))?; let port = uri.corr_port(); let stream = match connect_timeout { @@ -43,8 +44,9 @@ impl Stream { /// Tries to establish a secure connection over TLS. /// /// Checks if `uri` scheme denotes a HTTPS protocol: - /// - If yes, attemps to establish a secure connection + /// - If yes, attempts to establish a secure connection /// - Otherwise, returns the `stream` without any modification + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] pub fn try_to_https( stream: Stream, uri: &Uri, @@ -53,10 +55,7 @@ impl Stream { match stream { Stream::Http(http_stream) => { if uri.scheme() == "https" { - let host = match uri.host() { - Some(h) => h, - None => return Err(Error::Parse(ParseErr::UriErr)), - }; + let host = uri.host().ok_or(Error::Parse(ParseErr::UriErr))?; let mut cnf = tls::Config::default(); let cnf = match root_cert_file_pem { @@ -78,6 +77,7 @@ impl Stream { pub fn set_read_timeout(&mut self, dur: Option) -> Result<(), Error> { match self { Stream::Http(stream) => Ok(stream.set_read_timeout(dur)?), + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] Stream::Https(conn) => Ok(conn.get_mut().set_read_timeout(dur)?), } } @@ -86,6 +86,7 @@ impl Stream { pub fn set_write_timeout(&mut self, dur: Option) -> Result<(), Error> { match self { Stream::Http(stream) => Ok(stream.set_write_timeout(dur)?), + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] Stream::Https(conn) => Ok(conn.get_mut().set_write_timeout(dur)?), } } @@ -95,6 +96,7 @@ impl Read for Stream { fn read(&mut self, buf: &mut [u8]) -> Result { match self { Stream::Http(stream) => stream.read(buf), + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] Stream::Https(stream) => stream.read(buf), } } @@ -104,12 +106,15 @@ impl Write for Stream { fn write(&mut self, buf: &[u8]) -> Result { match self { Stream::Http(stream) => stream.write(buf), + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] Stream::Https(stream) => stream.write(buf), } } + fn flush(&mut self) -> Result<(), io::Error> { match self { Stream::Http(stream) => stream.flush(), + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] Stream::Https(stream) => stream.flush(), } } @@ -141,7 +146,7 @@ where Ok(0) | Err(_) => break, Ok(len) => { let filled_buf = buf[..len].to_vec(); - if let Err(_) = sender.send(filled_buf) { + if sender.send(filled_buf).is_err() { break; } } @@ -156,7 +161,7 @@ pub trait ThreadReceive { /// Fails if `deadline` is exceeded. fn receive(&mut self, receiver: &Receiver>, deadline: Instant) -> Result<(), Error>; - /// Continuosly receives data from `receiver` until there is no more data + /// Continuously receives data from `receiver` until there is no more data /// or `deadline` is exceeded. Writes received data into this writer. fn receive_all(&mut self, receiver: &Receiver>, deadline: Instant) -> Result<(), Error>; @@ -168,9 +173,12 @@ where { fn receive(&mut self, receiver: &Receiver>, deadline: Instant) -> Result<(), Error> { let now = Instant::now(); - let data_read = receiver.recv_timeout(deadline - now)?; - Ok(self.write_all(&data_read)?) + match receiver.recv_timeout(deadline - now) { + Ok(data_read) => self.write_all(&data_read).map_err(Error::IO), + Err(RecvTimeoutError::Timeout) => Err(Error::Timeout), + Err(RecvTimeoutError::Disconnected) => Ok(()), + } } fn receive_all( @@ -179,16 +187,16 @@ where deadline: Instant, ) -> Result<(), Error> { execute_with_deadline(deadline, |remaining_time| { - let data_read = match receiver.recv_timeout(remaining_time) { - Ok(data) => data, - Err(e) => match e { - RecvTimeoutError::Timeout => return Err(Error::Timeout), - RecvTimeoutError::Disconnected => return Ok(true), - }, - }; - - self.write_all(&data_read).map_err(|e| Error::IO(e))?; - Ok(false) + match receiver.recv_timeout(remaining_time) { + Ok(data_read) => { + if let Err(e) = self.write_all(&data_read) { + return Err(Error::IO(e)); + } + Ok(false) + } + Err(RecvTimeoutError::Timeout) => Err(Error::Timeout), + Err(RecvTimeoutError::Disconnected) => Ok(true), + } }) } } @@ -224,17 +232,17 @@ where )) } -/// Exexcutes a function in a loop until operation is completed or deadline is exceeded. +/// Executes 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 times a specific function can be called before the deadline. +/// For `execute_with_deadline` to meet the deadline, each call +/// to `func` needs to 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 `Some(true)` when the operation is complete, and `Some(false)` - when operation is in progress +/// - needs to return `Some(true)` when the operation is complete, and `Some(false)` - when the operation is in progress pub fn execute_with_deadline(deadline: Instant, mut func: F) -> Result<(), Error> where F: FnMut(Duration) -> Result, @@ -260,7 +268,7 @@ where /// Reads the head of HTTP response from `reader`. /// /// Reads from `reader` (line by line) until a blank line is identified, -/// which indicates that all meta-information has been read, +/// which indicates that all meta-information has been read. pub fn read_head(reader: &mut B) -> Vec where B: BufRead, @@ -319,6 +327,7 @@ mod tests { } #[test] + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] fn stream_try_to_https() { { let uri = Uri::try_from(URI_S).unwrap(); @@ -363,6 +372,7 @@ mod tests { assert_eq!(inner_read_timeout, Some(TIMEOUT)); } + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] { let uri = Uri::try_from(URI_S).unwrap(); let mut stream = Stream::connect(&uri, None).unwrap(); @@ -394,6 +404,7 @@ mod tests { assert_eq!(inner_read_timeout, Some(TIMEOUT)); } + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] { let uri = Uri::try_from(URI_S).unwrap(); let mut stream = Stream::connect(&uri, None).unwrap(); diff --git a/src/tls.rs b/src/tls.rs index 2357c8c..bad4267 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -1,4 +1,5 @@ //! secure connection over TLS + use crate::error::Error as HttpError; use std::{ fs::File, @@ -15,7 +16,7 @@ use rustls::{ClientConnection, StreamOwned}; 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"); +compile_error!("One of the `native-tls` or `rust-tls` features must be enabled"); /// Wrapper around TLS Stream, depends on selected TLS library: /// - native_tls: `TlsStream` @@ -53,12 +54,9 @@ where #[cfg(feature = "rust-tls")] { - // TODO: this api returns ConnectionAborted with a "..CloseNotify.." string. - // TODO: we should work out if self.stream.sess exposes enough information - // TODO: to not read in this situation, and return EOF directly. - // TODO: c.f. the checks in the implementation. connection_at_eof() doesn't - // TODO: seem to be exposed. The implementation: - // TODO: https://github.com/ctz/rustls/blob/f93c325ce58f2f1e02f09bcae6c48ad3f7bde542/src/session.rs#L789-L792 + // Handle ConnectionAborted for Rust-TLS + // Reference to the rustls implementation: + // https://github.com/ctz/rustls/blob/f93c325ce58f2f1e02f09bcae6c48ad3f7bde542/src/session.rs#L789-L792 if let Err(ref e) = len { if io::ErrorKind::ConnectionAborted == e.kind() { return Ok(0); @@ -77,6 +75,7 @@ where fn write(&mut self, buf: &[u8]) -> Result { self.stream.write(buf) } + fn flush(&mut self) -> Result<(), io::Error> { self.stream.flush() } @@ -86,6 +85,7 @@ where pub struct Config { #[cfg(feature = "native-tls")] extra_root_certs: Vec, + #[cfg(feature = "rust-tls")] root_certs: std::sync::Arc, } diff --git a/src/uri.rs b/src/uri.rs index 6f55b6b..a1863ac 100644 --- a/src/uri.rs +++ b/src/uri.rs @@ -1,4 +1,5 @@ //! uri operations + use crate::error::{Error, ParseErr}; use std::{ convert::TryFrom, @@ -21,11 +22,14 @@ pub struct RangeC { impl RangeC { /// Creates new `RangeC` with `start` and `end`. /// - /// # Exmaples + /// # Examples /// ``` /// use http_req::uri::RangeC; /// - /// const range: RangeC = RangeC::new(0, 20); + /// let range = RangeC::new(0, 20); + /// + /// assert_eq!(range.start, 0); + /// assert_eq!(range.end, 20); /// ``` pub const fn new(start: usize, end: usize) -> RangeC { RangeC { start, end } @@ -61,12 +65,12 @@ impl Index for String { /// Representation of Uniform Resource Identifier /// -/// # Example +/// # Examples /// ``` /// use http_req::uri::Uri; /// use std::convert::TryFrom; /// -/// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap();; +/// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); /// assert_eq!(uri.host(), Some("foo.com")); /// ``` #[derive(Clone, Debug, PartialEq)] @@ -87,12 +91,12 @@ impl<'a> Uri<'a> { /// Returns scheme of this `Uri`. /// - /// # Example + /// # Examples /// ``` /// use http_req::uri::Uri; /// use std::convert::TryFrom; /// - /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap();; + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); /// assert_eq!(uri.scheme(), "https"); /// ``` pub fn scheme(&self) -> &str { @@ -101,12 +105,12 @@ impl<'a> Uri<'a> { /// Returns information about the user included in this `Uri`. /// - /// # Example + /// # Examples /// ``` /// use http_req::uri::Uri; /// use std::convert::TryFrom; /// - /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap();; + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); /// assert_eq!(uri.user_info(), Some("user:info")); /// ``` pub fn user_info(&self) -> Option<&str> { @@ -115,12 +119,12 @@ impl<'a> Uri<'a> { /// Returns host of this `Uri`. /// - /// # Example + /// # Examples /// ``` /// use http_req::uri::Uri; /// use std::convert::TryFrom; /// - /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap();; + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); /// assert_eq!(uri.host(), Some("foo.com")); /// ``` pub fn host(&self) -> Option<&str> { @@ -129,12 +133,12 @@ impl<'a> Uri<'a> { /// Returns host of this `Uri` to use in a header. /// - /// # Example + /// # Examples /// ``` /// use http_req::uri::Uri; /// use std::convert::TryFrom; /// - /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap();; + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); /// assert_eq!(uri.host_header(), Some("foo.com:12".to_string())); /// ``` pub fn host_header(&self) -> Option { @@ -146,12 +150,12 @@ impl<'a> Uri<'a> { /// Returns port of this `Uri` /// - /// # Example + /// # Examples /// ``` /// use http_req::uri::Uri; /// use std::convert::TryFrom; /// - /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap();; + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); /// assert_eq!(uri.port(), Some(12)); /// ``` pub fn port(&self) -> Option { @@ -161,12 +165,12 @@ impl<'a> Uri<'a> { /// Returns port corresponding to this `Uri`. /// Returns default port if it hasn't been set in the uri. /// - /// # Example + /// # Examples /// ``` /// use http_req::uri::Uri; /// use std::convert::TryFrom; /// - /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap();; + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); /// assert_eq!(uri.corr_port(), 12); /// ``` pub fn corr_port(&self) -> u16 { @@ -183,12 +187,12 @@ impl<'a> Uri<'a> { /// Returns path of this `Uri`. /// - /// # Example + /// # Examples /// ``` /// use http_req::uri::Uri; /// use std::convert::TryFrom; /// - /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap();; + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); /// assert_eq!(uri.path(), Some("/bar/baz")); /// ``` pub fn path(&self) -> Option<&str> { @@ -197,12 +201,12 @@ impl<'a> Uri<'a> { /// Returns query of this `Uri`. /// - /// # Example + /// # Examples /// ``` /// use http_req::uri::Uri; /// use std::convert::TryFrom; /// - /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap();; + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); /// assert_eq!(uri.query(), Some("query")); /// ``` pub fn query(&self) -> Option<&str> { @@ -211,12 +215,12 @@ impl<'a> Uri<'a> { /// Returns fragment of this `Uri`. /// - /// # Example + /// # Examples /// ``` /// use http_req::uri::Uri; /// use std::convert::TryFrom; /// - /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap();; + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); /// assert_eq!(uri.fragment(), Some("fragment")); /// ``` pub fn fragment(&self) -> Option<&str> { @@ -225,12 +229,12 @@ impl<'a> Uri<'a> { /// Returns resource `Uri` points to. /// - /// # Example + /// # Examples /// ``` /// use http_req::uri::Uri; /// use std::convert::TryFrom; /// - /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap();; + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); /// assert_eq!(uri.resource(), "/bar/baz?query#fragment"); /// ``` pub fn resource(&self) -> &str { @@ -241,6 +245,14 @@ impl<'a> Uri<'a> { } /// Checks if &str is a relative uri. + /// + /// # Examples + /// ``` + /// use http_req::uri::Uri; + /// + /// assert!(Uri::is_relative("/relative/path")); + /// assert!(!Uri::is_relative("http://absolute.com")); + /// ``` pub fn is_relative(raw_uri: &str) -> bool { raw_uri.starts_with("/") || raw_uri.starts_with("?") @@ -250,6 +262,18 @@ impl<'a> Uri<'a> { /// Creates a new `Uri` from current uri and relative uri. /// Writes the new uri (raw string) into `relative_uri`. + /// + /// # Examples + /// ``` + /// use http_req::uri::Uri; + /// use std::convert::TryFrom; + /// + /// let base_uri: Uri = Uri::try_from("https://example.com/base").unwrap(); + /// let mut relative_path = String::from("/relative/path"); + /// let new_uri = base_uri.from_relative(&mut relative_path).unwrap(); + /// + /// assert_eq!(new_uri.to_string(), "https://example.com/relative/path"); + /// ``` 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(); @@ -258,7 +282,7 @@ impl<'a> Uri<'a> { 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, "/"), + _ => Uri::add_part_end(&resource, relative_uri, "/"), }; *relative_uri = if let Some(p) = self.path { @@ -270,7 +294,7 @@ impl<'a> Uri<'a> { Uri::try_from(relative_uri.as_str()) } - /// Adds a part at the beggining of the base. + /// Adds a part at the beginning 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 { @@ -377,7 +401,7 @@ impl<'a> TryFrom<&'a str> for Uri<'a> { /// Authority of Uri /// -/// # Example +/// # Examples /// ``` /// use http_req::uri::Authority; /// use std::convert::TryFrom; @@ -397,7 +421,7 @@ pub struct Authority<'a> { impl<'a> Authority<'a> { /// Returns username of this `Authority` /// - /// # Example + /// # Examples /// ``` /// use http_req::uri::Authority; /// use std::convert::TryFrom; @@ -411,7 +435,7 @@ impl<'a> Authority<'a> { /// Returns password of this `Authority` /// - /// # Example + /// # Examples /// ``` /// use http_req::uri::Authority; /// use std::convert::TryFrom; @@ -425,7 +449,7 @@ impl<'a> Authority<'a> { /// Returns information about the user /// - /// # Example + /// # Examples /// ``` /// use http_req::uri::Authority; /// use std::convert::TryFrom; @@ -443,7 +467,7 @@ impl<'a> Authority<'a> { /// Returns host of this `Authority` /// - /// # Example + /// # Examples /// ``` /// use http_req::uri::Authority; /// use std::convert::TryFrom; @@ -457,7 +481,7 @@ impl<'a> Authority<'a> { /// Returns port of this `Authority` /// - /// # Example + /// # Examples /// ``` /// use http_req::uri::Authority; /// use std::convert::TryFrom; @@ -524,11 +548,6 @@ impl<'a> fmt::Display for Authority<'a> { } } -/// Removes whitespace from `text` -pub fn remove_spaces(text: &mut String) { - text.retain(|c| !c.is_whitespace()); -} - /// Splits `s` by `separator`. If `separator` is found inside `s`, it will return two `Some` values /// consisting `RangeC` of each `&str`. If `separator` is at the end of `s` or it's not found, /// it will return tuple consisting `Some` with `RangeC` of entire `s` inside and None. @@ -588,15 +607,6 @@ mod tests { "?users#1551", ]; - #[test] - fn remove_space() { - let mut text = String::from("Hello World !"); - let expect = String::from("HelloWorld!"); - - remove_spaces(&mut text); - assert_eq!(text, expect); - } - #[test] fn uri_full_parse() { let uri = Uri::try_from(