diff --git a/Cargo.lock b/Cargo.lock index 16e0ae8..d30b8c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -214,6 +214,7 @@ dependencies = [ name = "http_req" version = "0.13.0" dependencies = [ + "base64", "native-tls", "rustls", "rustls-pemfile", @@ -221,6 +222,7 @@ dependencies = [ "unicase", "webpki", "webpki-roots", + "zeroize", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index de4d345..c1c4d78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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/src/request.rs b/src/request.rs index 4db9ba2..82be086 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,6 +16,7 @@ use std::{ thread, time::{Duration, Instant}, }; +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; const CR_LF: &str = "\r\n"; const DEFAULT_REDIRECT_LIMIT: usize = 5; @@ -85,6 +87,79 @@ impl fmt::Display for HttpVersion { } } +/// Authentication details: +/// - Basic: username and password +/// - Bearer: token +#[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop)] +pub struct Authentication(AuthenticationType); + +impl Authentication { + /// Creates a new `Authentication` of type `Basic`. + 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` + 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 + 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()), + } + } +} + /// Allows to control redirects #[derive(Debug, PartialEq, Clone, Copy)] pub enum RedirectPolicy { @@ -258,6 +333,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 @@ -456,6 +554,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 @@ -784,6 +902,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()); @@ -831,6 +986,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(); diff --git a/src/response.rs b/src/response.rs index 81b9e32..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