diff --git a/Cargo.toml b/Cargo.toml index e354feb..af19025 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "http-sig" description = "Implementation of the IETF draft 'Signing HTTP Messages'" -version = "0.5.0" +version = "0.6.0" authors = ["Jack Cargill ", "Diggory Blake"] edition = "2018" readme = "README.md" diff --git a/src/lib.rs b/src/lib.rs index 095ad70..4bf6b8e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,8 +83,6 @@ pub mod mock_request; #[cfg(feature = "reqwest")] mod reqwest_impls; -#[cfg(feature = "reqwest")] -pub use reqwest_impls::*; #[cfg(feature = "rouille")] mod rouille_impls; diff --git a/src/reqwest_impls.rs b/src/reqwest_impls.rs index ff5c123..d2b13cf 100644 --- a/src/reqwest_impls.rs +++ b/src/reqwest_impls.rs @@ -4,6 +4,14 @@ use http::header::{HeaderName, HeaderValue}; use super::*; +/// Returns the correct `Host` header value for a given URL, in the form `:`. +fn host_from_url(url: &url::Url) -> Option { + url.host_str().map(|host| match url.port() { + Some(port) => format!("{}:{}", host, port), + None => host.into(), + }) +} + impl RequestLike for reqwest::Request { fn header(&self, header: &Header) -> Option { match header { @@ -20,7 +28,7 @@ impl RequestLike for reqwest::Request { impl ClientRequestLike for reqwest::Request { fn host(&self) -> Option { - self.url().host_str().map(Into::into) + host_from_url(self.url()) } fn compute_digest(&mut self, digest: &dyn HttpDigest) -> Option { self.body()?.as_bytes().map(|b| digest.http_digest(b)) @@ -52,7 +60,7 @@ impl RequestLike for reqwest::blocking::Request { impl ClientRequestLike for reqwest::blocking::Request { fn host(&self) -> Option { - self.url().host_str().map(Into::into) + host_from_url(self.url()) } fn compute_digest(&mut self, digest: &dyn HttpDigest) -> Option { let bytes_to_digest = self.body_mut().as_mut()?.buffer().ok()?; @@ -66,7 +74,7 @@ impl ClientRequestLike for reqwest::blocking::Request { #[cfg(test)] mod tests { use chrono::{offset::TimeZone, Utc}; - use http::header::{AUTHORIZATION, CONTENT_TYPE, DATE}; + use http::header::{AUTHORIZATION, CONTENT_TYPE, DATE, HOST}; use super::*; @@ -81,8 +89,9 @@ mod tests { .header(CONTENT_TYPE, "application/json") .header( DATE, - Utc.ymd(2014, 7, 8) - .and_hms(9, 10, 11) + Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11) + .single() + .expect("valid date") .format("%a, %d %b %Y %T GMT") .to_string(), ) @@ -100,8 +109,90 @@ mod tests { .unwrap(), "SHA-256=2vgEVkfe4d6VW+tSWAziO7BUx7uT/rA9hn1EoxUJi2o=" ); + assert_eq!(with_sig.headers().get(HOST).unwrap(), "test.com"); + } + + #[test] + fn it_works_blocking() { + let config = SigningConfig::new_default("test_key", "abcdefgh".as_bytes()); + + let client = reqwest::blocking::Client::new(); + + let without_sig = client + .post("http://test.com/foo/bar") + .header(CONTENT_TYPE, "application/json") + .header( + DATE, + Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11) + .single() + .expect("valid date") + .format("%a, %d %b %Y %T GMT") + .to_string(), + ) + .body(&br#"{ "x": 1, "y": 2}"#[..]) + .build() + .unwrap(); + + let with_sig = without_sig.signed(&config).unwrap(); + + assert_eq!(with_sig.headers().get(AUTHORIZATION).unwrap(), "Signature keyId=\"test_key\",algorithm=\"hs2019\",signature=\"F8gZiriO7dtKFiP5eSZ+Oh1h61JIrAR6D5Mdh98DjqA=\",headers=\"(request-target) host date digest\""); + assert_eq!( + with_sig + .headers() + .get(HeaderName::from_static("digest")) + .unwrap(), + "SHA-256=2vgEVkfe4d6VW+tSWAziO7BUx7uT/rA9hn1EoxUJi2o=" + ); + assert_eq!(with_sig.headers().get(HOST).unwrap(), "test.com"); + } + + #[test] + fn sets_host_header_with_port_correctly() { + let config = SigningConfig::new_default("test_key", "abcdefgh".as_bytes()); + let client = reqwest::Client::new(); + + let without_sig = client + .post("http://localhost:8080/foo/bar") + .header(CONTENT_TYPE, "application/json") + .header( + DATE, + Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11) + .single() + .expect("valid date") + .format("%a, %d %b %Y %T GMT") + .to_string(), + ) + .body(&br#"{ "x": 1, "y": 2}"#[..]) + .build() + .unwrap(); + + let with_sig = without_sig.signed(&config).unwrap(); + assert_eq!(with_sig.headers().get(HOST).unwrap(), "localhost:8080"); } + #[test] + fn sets_host_header_with_port_correctly_blocking() { + let config = SigningConfig::new_default("test_key", "abcdefgh".as_bytes()); + let client = reqwest::blocking::Client::new(); + + let without_sig = client + .post("http://localhost:8080/foo/bar") + .header(CONTENT_TYPE, "application/json") + .header( + DATE, + Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11) + .single() + .expect("valid date") + .format("%a, %d %b %Y %T GMT") + .to_string(), + ) + .body(&br#"{ "x": 1, "y": 2}"#[..]) + .build() + .unwrap(); + + let with_sig = without_sig.signed(&config).unwrap(); + assert_eq!(with_sig.headers().get(HOST).unwrap(), "localhost:8080"); + } #[test] #[ignore] fn it_can_talk_to_reference_integration() { diff --git a/src/rouille_impls.rs b/src/rouille_impls.rs index 9a18ac8..cf16eb5 100644 --- a/src/rouille_impls.rs +++ b/src/rouille_impls.rs @@ -103,14 +103,13 @@ mod tests { vec![ ("Host".into(), "test.com".into()), ("ContentType".into(), "application/json".into()), - ("Date".into(), Utc.ymd(2014, 7, 8) - .and_hms(9, 10, 11) + ("Date".into(), Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11).single().expect("valid date") .format("%a, %d %b %Y %T GMT") .to_string()), ("Digest".into(), "SHA-256=2vgEVkfe4d6VW+tSWAziO7BUx7uT/rA9hn1EoxUJi2o=".into()), ("Authorization".into(), "Signature keyId=\"test_key\",algorithm=\"hmac-sha256\",signature=\"uH2I9FSuCGUrIEygs7hR29oz0Afkz0bZyHpz6cW/mLQ=\",headers=\"(request-target) date digest host".into()), ], - br#"{ "x": 1, "y": 2}"#[..].into() + br#"{ "x": 1, "y": 2}"#[..].into(), ); request.verify(&config).unwrap(); @@ -129,13 +128,13 @@ mod tests { "GET", "/foo/bar", vec![ - ("Date".into(), Utc.ymd(2014, 7, 8) - .and_hms(9, 10, 11) + ("Date".into(), Utc.with_ymd_and_hms(2014, 7, 8, + 9, 10, 11).single().expect("valid date") .format("%a, %d %b %Y %T GMT") .to_string()), ("Authorization".into(), "Signature keyId=\"test_key\",algorithm=\"hmac-sha256\",signature=\"sGQ3hA9KB40CU1pHbRLXLvLdUWYn+c3fcfL+Sw8kIZE=\",headers=\"(request-target) date".into()), ], - Vec::new() + Vec::new(), ); request.verify(&config).unwrap(); diff --git a/src/signing.rs b/src/signing.rs index 5c76dba..e62cc1e 100644 --- a/src/signing.rs +++ b/src/signing.rs @@ -18,8 +18,9 @@ use crate::{DefaultDigestAlgorithm, DefaultSignatureAlgorithm, DATE_FORMAT}; /// HTTP request. The HTTP signing extension methods are available on /// any type implementing this trait. pub trait ClientRequestLike: RequestLike { - /// Returns the host for the request (eg. "example.com") in case the Host header has - /// not been set explicitly. + /// Returns the host for the request (eg. "example.com", "127.0.0.1:8080") in case the Host header has + /// not been set explicitly. Note, the correct form of the `Host` header is `:` if + /// the port is non-standard for the protocol used (e.g., 443 for an HTTPS URL, and 80 for an HTTP URL). /// When implementing this trait, do not just read the `Host` header from the request - /// this method will only be called when the `Host` header is not set. fn host(&self) -> Option { diff --git a/src/verifying.rs b/src/verifying.rs index e9e2398..db8c337 100644 --- a/src/verifying.rs +++ b/src/verifying.rs @@ -506,7 +506,7 @@ fn verify_except_digest( })?; // Then parse into a datetime - let provided_date = DateTime::::from_utc( + let provided_date = DateTime::::from_naive_utc_and_offset( NaiveDateTime::parse_from_str(date_value, DATE_FORMAT) .ok() .or_else(|| {