summaryrefslogtreecommitdiffstats
path: root/third_party/rust/hawk/src/request.rs
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/rust/hawk/src/request.rs')
-rw-r--r--third_party/rust/hawk/src/request.rs974
1 files changed, 974 insertions, 0 deletions
diff --git a/third_party/rust/hawk/src/request.rs b/third_party/rust/hawk/src/request.rs
new file mode 100644
index 0000000000..4cccab20d1
--- /dev/null
+++ b/third_party/rust/hawk/src/request.rs
@@ -0,0 +1,974 @@
+use crate::bewit::Bewit;
+use crate::credentials::{Credentials, Key};
+use crate::error::*;
+use crate::header::Header;
+use crate::mac::{Mac, MacType};
+use crate::response::ResponseBuilder;
+use log::debug;
+use std::borrow::Cow;
+use std::str;
+use std::str::FromStr;
+use std::time::{Duration, SystemTime};
+use url::{Position, Url};
+
+/// Request represents a single HTTP request.
+///
+/// The structure is created using (RequestBuilder)[struct.RequestBuilder.html]. Most uses of this
+/// library will hold several of the fields in this structure fixed. Cloning the structure with
+/// these fields applied is a convenient way to avoid repeating those fields. Most fields are
+/// references, since in common use the values already exist and will outlive the request.
+///
+/// A request can be used on the client, to generate a header or a bewit, or on the server, to
+/// validate the same.
+///
+/// # Examples
+///
+/// ```
+/// use hawk::RequestBuilder;
+/// let bldr = RequestBuilder::new("GET", "mysite.com", 443, "/");
+/// let request1 = bldr.clone().method("POST").path("/api/user").request();
+/// let request2 = bldr.path("/api/users").request();
+/// ```
+///
+/// See the documentation in the crate root for examples of creating and validating headers.
+#[derive(Debug, Clone)]
+pub struct Request<'a> {
+ method: &'a str,
+ host: &'a str,
+ port: u16,
+ path: Cow<'a, str>,
+ hash: Option<&'a [u8]>,
+ ext: Option<&'a str>,
+ app: Option<&'a str>,
+ dlg: Option<&'a str>,
+}
+
+impl<'a> Request<'a> {
+ /// Create a new Header for this request, inventing a new nonce and setting the
+ /// timestamp to the current time.
+ pub fn make_header(&self, credentials: &Credentials) -> Result<Header> {
+ let nonce = random_string(10)?;
+ self.make_header_full(credentials, SystemTime::now(), nonce)
+ }
+
+ /// Similar to `make_header`, but allowing specification of the timestamp
+ /// and nonce.
+ pub fn make_header_full<S>(
+ &self,
+ credentials: &Credentials,
+ ts: SystemTime,
+ nonce: S,
+ ) -> Result<Header>
+ where
+ S: Into<String>,
+ {
+ let nonce = nonce.into();
+ let mac = Mac::new(
+ MacType::Header,
+ &credentials.key,
+ ts,
+ &nonce,
+ self.method,
+ self.host,
+ self.port,
+ self.path.as_ref(),
+ self.hash,
+ self.ext,
+ )?;
+ Header::new(
+ Some(credentials.id.clone()),
+ Some(ts),
+ Some(nonce),
+ Some(mac),
+ match self.ext {
+ None => None,
+ Some(v) => Some(v.to_string()),
+ },
+ match self.hash {
+ None => None,
+ Some(v) => Some(v.to_vec()),
+ },
+ match self.app {
+ None => None,
+ Some(v) => Some(v.to_string()),
+ },
+ match self.dlg {
+ None => None,
+ Some(v) => Some(v.to_string()),
+ },
+ )
+ }
+
+ /// Make a "bewit" that can be attached to a URL to authenticate GET access.
+ ///
+ /// The ttl gives the time for which this bewit is valid, starting now.
+ pub fn make_bewit(&self, credentials: &'a Credentials, exp: SystemTime) -> Result<Bewit<'a>> {
+ // note that this includes `method` and `hash` even though they must always be GET and None
+ // for bewits. If they aren't, then the bewit just won't validate -- no need to catch
+ // that now
+ let mac = Mac::new(
+ MacType::Bewit,
+ &credentials.key,
+ exp,
+ "",
+ self.method,
+ self.host,
+ self.port,
+ self.path.as_ref(),
+ self.hash,
+ self.ext,
+ )?;
+ let bewit = Bewit::new(&credentials.id, exp, mac, self.ext);
+ Ok(bewit)
+ }
+
+ /// Variant of `make_bewit` that takes a Duration (starting from now)
+ /// instead of a SystemTime, provided for convenience.
+ pub fn make_bewit_with_ttl(
+ &self,
+ credentials: &'a Credentials,
+ ttl: Duration,
+ ) -> Result<Bewit<'a>> {
+ let exp = SystemTime::now() + ttl;
+ self.make_bewit(credentials, exp)
+ }
+
+ /// Validate the given header. This validates that the `mac` field matches that calculated
+ /// using the other header fields and the given request information.
+ ///
+ /// The header's timestamp is verified to be within `ts_skew` of the current time. If any of
+ /// the required header fields are missing, the method will return false.
+ ///
+ /// It is up to the caller to examine the header's `id` field and supply the corresponding key.
+ ///
+ /// If desired, it is up to the caller to validate that `nonce` has not been used before.
+ ///
+ /// If a hash has been supplied, then the header must contain a matching hash. Note that this
+ /// hash must be calculated based on the request body, not copied from the request header!
+ pub fn validate_header(&self, header: &Header, key: &Key, ts_skew: Duration) -> bool {
+ // extract required fields, returning early if they are not present
+ let ts = match header.ts {
+ Some(ts) => ts,
+ None => {
+ debug!("missing timestamp from header");
+ return false;
+ }
+ };
+ let nonce = match header.nonce {
+ Some(ref nonce) => nonce,
+ None => {
+ debug!("missing nonce from header");
+ return false;
+ }
+ };
+ let header_mac = match header.mac {
+ Some(ref mac) => mac,
+ None => {
+ debug!("missing mac from header");
+ return false;
+ }
+ };
+ let header_hash = match header.hash {
+ Some(ref hash) => Some(&hash[..]),
+ None => None,
+ };
+ let header_ext = match header.ext {
+ Some(ref ext) => Some(&ext[..]),
+ None => None,
+ };
+
+ // first verify the MAC
+ match Mac::new(
+ MacType::Header,
+ key,
+ ts,
+ nonce,
+ self.method,
+ self.host,
+ self.port,
+ self.path.as_ref(),
+ header_hash,
+ header_ext,
+ ) {
+ Ok(calculated_mac) => {
+ if &calculated_mac != header_mac {
+ debug!("calculated mac doesn't match header");
+ return false;
+ }
+ }
+ Err(e) => {
+ debug!("unexpected mac error: {:?}", e);
+ return false;
+ }
+ };
+
+ // ..then the hashes
+ if let Some(local_hash) = self.hash {
+ if let Some(server_hash) = header_hash {
+ if local_hash != server_hash {
+ debug!("server hash doesn't match header");
+ return false;
+ }
+ } else {
+ debug!("missing hash from header");
+ return false;
+ }
+ }
+
+ // ..then the timestamp
+ let now = SystemTime::now();
+ let skew = if now > ts {
+ now.duration_since(ts).unwrap()
+ } else {
+ ts.duration_since(now).unwrap()
+ };
+ if skew > ts_skew {
+ debug!(
+ "bad timestamp skew, timestamp too old? detected skew: {:?}, ts_skew: {:?}",
+ &skew, &ts_skew
+ );
+ return false;
+ }
+
+ true
+ }
+
+ /// Validate the given bewit matches this request.
+ ///
+ /// It is up to the caller to consult the Bewit's `id` and look up the
+ /// corresponding key.
+ ///
+ /// Nonces and hashes do not apply when using bewits.
+ pub fn validate_bewit(&self, bewit: &Bewit, key: &Key) -> bool {
+ let calculated_mac = Mac::new(
+ MacType::Bewit,
+ key,
+ bewit.exp(),
+ "",
+ self.method,
+ self.host,
+ self.port,
+ self.path.as_ref(),
+ self.hash,
+ match bewit.ext() {
+ Some(e) => Some(e),
+ None => None,
+ },
+ );
+ let calculated_mac = match calculated_mac {
+ Ok(m) => m,
+ Err(_) => {
+ return false;
+ }
+ };
+
+ if bewit.mac() != &calculated_mac {
+ return false;
+ }
+
+ let now = SystemTime::now();
+ if bewit.exp() < now {
+ return false;
+ }
+
+ true
+ }
+
+ /// Get a Response instance for a response to this request. This is a convenience
+ /// wrapper around `Response::from_request_header`.
+ pub fn make_response_builder(&'a self, req_header: &'a Header) -> ResponseBuilder<'a> {
+ ResponseBuilder::from_request_header(
+ req_header,
+ self.method,
+ self.host,
+ self.port,
+ self.path.as_ref(),
+ )
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct RequestBuilder<'a>(Request<'a>);
+
+impl<'a> RequestBuilder<'a> {
+ /// Create a new request with the given method, host, port, and path.
+ pub fn new(method: &'a str, host: &'a str, port: u16, path: &'a str) -> Self {
+ RequestBuilder(Request {
+ method,
+ host,
+ port,
+ path: Cow::Borrowed(path),
+ hash: None,
+ ext: None,
+ app: None,
+ dlg: None,
+ })
+ }
+
+ /// Create a new request with the host, port, and path determined from the URL.
+ pub fn from_url(method: &'a str, url: &'a Url) -> Result<Self> {
+ let (host, port, path) = RequestBuilder::parse_url(url)?;
+ Ok(RequestBuilder(Request {
+ method,
+ host,
+ port,
+ path: Cow::Borrowed(path),
+ hash: None,
+ ext: None,
+ app: None,
+ dlg: None,
+ }))
+ }
+
+ /// Set the request method. This should be a capitalized string.
+ pub fn method(mut self, method: &'a str) -> Self {
+ self.0.method = method;
+ self
+ }
+
+ /// Set the URL path for the request.
+ pub fn path(mut self, path: &'a str) -> Self {
+ self.0.path = Cow::Borrowed(path);
+ self
+ }
+
+ /// Set the URL hostname for the request
+ pub fn host(mut self, host: &'a str) -> Self {
+ self.0.host = host;
+ self
+ }
+
+ /// Set the URL port for the request
+ pub fn port(mut self, port: u16) -> Self {
+ self.0.port = port;
+ self
+ }
+
+ /// Set the hostname, port, and path for the request, from a string URL.
+ pub fn url(self, url: &'a Url) -> Result<Self> {
+ let (host, port, path) = RequestBuilder::parse_url(url)?;
+ Ok(self.path(path).host(host).port(port))
+ }
+
+ /// Set the content hash for the request
+ pub fn hash<H: Into<Option<&'a [u8]>>>(mut self, hash: H) -> Self {
+ self.0.hash = hash.into();
+ self
+ }
+
+ /// Set the `ext` Hawk property for the request
+ pub fn ext<S: Into<Option<&'a str>>>(mut self, ext: S) -> Self {
+ self.0.ext = ext.into();
+ self
+ }
+
+ /// Set the `app` Hawk property for the request
+ pub fn app<S: Into<Option<&'a str>>>(mut self, app: S) -> Self {
+ self.0.app = app.into();
+ self
+ }
+
+ /// Set the `dlg` Hawk property for the request
+ pub fn dlg<S: Into<Option<&'a str>>>(mut self, dlg: S) -> Self {
+ self.0.dlg = dlg.into();
+ self
+ }
+
+ /// Get the request from this builder
+ pub fn request(self) -> Request<'a> {
+ self.0
+ }
+
+ /// Extract the `bewit` query parameter, if any, from the path, and return it in the output
+ /// parameter, returning a modified RequestBuilder omitting the `bewit=..` query parameter. If
+ /// no bewit is present, or if an error is returned, the output parameter is reset to None.
+ ///
+ /// The path manipulation is tested to correspond to that preformed by the hueniverse/hawk
+ /// implementation-specification
+ pub fn extract_bewit(mut self, bewit: &mut Option<Bewit<'a>>) -> Result<Self> {
+ const PREFIX: &str = "bewit=";
+ *bewit = None;
+
+ if let Some(query_index) = self.0.path.find('?') {
+ let (bewit_components, components): (Vec<&str>, Vec<&str>) = self.0.path
+ [query_index + 1..]
+ .split('&')
+ .partition(|comp| comp.starts_with(PREFIX));
+
+ if bewit_components.len() == 1 {
+ let bewit_str = bewit_components[0];
+ *bewit = Some(Bewit::from_str(&bewit_str[PREFIX.len()..])?);
+
+ // update the path to omit the bewit=... segment
+ let new_path = if !components.is_empty() {
+ format!("{}{}", &self.0.path[..=query_index], components.join("&")).to_string()
+ } else {
+ // no query left, so return the remaining path, omitting the '?'
+ self.0.path[..query_index].to_string()
+ };
+ self.0.path = Cow::Owned(new_path);
+ Ok(self)
+ } else if bewit_components.is_empty() {
+ Ok(self)
+ } else {
+ Err(InvalidBewit::Multiple.into())
+ }
+ } else {
+ Ok(self)
+ }
+ }
+
+ fn parse_url(url: &'a Url) -> Result<(&'a str, u16, &'a str)> {
+ let host = url
+ .host_str()
+ .ok_or_else(|| Error::InvalidUrl(format!("url {} has no host", url)))?;
+ let port = url
+ .port_or_known_default()
+ .ok_or_else(|| Error::InvalidUrl(format!("url {} has no port", url)))?;
+ let path = &url[Position::BeforePath..];
+ Ok((host, port, path))
+ }
+}
+
+/// Create a random string with `bytes` bytes of entropy. The string
+/// is base64-encoded. so it will be longer than bytes characters.
+fn random_string(bytes: usize) -> Result<String> {
+ let mut bytes = vec![0u8; bytes];
+ crate::crypto::rand_bytes(&mut bytes)?;
+ Ok(base64::encode(&bytes))
+}
+
+#[cfg(all(test, any(feature = "use_ring", feature = "use_openssl")))]
+mod test {
+ use super::*;
+ use crate::credentials::{Credentials, Key};
+ use crate::header::Header;
+ use std::str::FromStr;
+ use std::time::{Duration, SystemTime, UNIX_EPOCH};
+ use url::Url;
+
+ // this is a header from a real request using the JS Hawk library, to
+ // https://pulse.taskcluster.net:443/v1/namespaces with credentials "me" / "tok"
+ const REAL_HEADER: &'static str = "id=\"me\", ts=\"1491183061\", nonce=\"RVnYzW\", \
+ mac=\"1kqRT9EoxiZ9AA/ayOCXB+AcjfK/BoJ+n7z0gfvZotQ=\"";
+ const BEWIT_STR: &str =
+ "bWVcMTM1MzgzMjgzNFxmaXk0ZTV3QmRhcEROeEhIZUExOE5yU3JVMVUzaVM2NmdtMFhqVEpwWXlVPVw";
+
+ // this is used as the initial bewit when calling extract_bewit, to verify that it is
+ // not allowing the original value of the parameter to remain in place.
+ const INITIAL_BEWIT_STR: &str =
+ "T0ggTk9FU1wxMzUzODMyODM0XGZpeTRlNXdCZGFwRE54SEhlQTE4TnJTclUxVTNpUzY2Z20wWGpUSnBZeVU9XCZtdXQgYmV3aXQgbm90IHJlc2V0IQ";
+
+ #[test]
+ fn test_empty() {
+ let req = RequestBuilder::new("GET", "site", 80, "/").request();
+ assert_eq!(req.method, "GET");
+ assert_eq!(req.host, "site");
+ assert_eq!(req.port, 80);
+ assert_eq!(req.path, "/");
+ assert_eq!(req.hash, None);
+ assert_eq!(req.ext, None);
+ assert_eq!(req.app, None);
+ assert_eq!(req.dlg, None);
+ }
+
+ #[test]
+ fn test_builder() {
+ let hash = vec![0u8];
+ let req = RequestBuilder::new("GET", "example.com", 443, "/foo")
+ .hash(Some(&hash[..]))
+ .ext("ext")
+ .app("app")
+ .dlg("dlg")
+ .request();
+
+ assert_eq!(req.method, "GET");
+ assert_eq!(req.path, "/foo");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443);
+ assert_eq!(req.hash, Some(&hash[..]));
+ assert_eq!(req.ext, Some("ext"));
+ assert_eq!(req.app, Some("app"));
+ assert_eq!(req.dlg, Some("dlg"));
+ }
+
+ #[test]
+ fn test_builder_clone() {
+ let rb = RequestBuilder::new("GET", "site", 443, "/foo");
+ let req = rb.clone().request();
+ let req2 = rb.path("/bar").request();
+
+ assert_eq!(req.method, "GET");
+ assert_eq!(req.path, "/foo");
+ assert_eq!(req2.method, "GET");
+ assert_eq!(req2.path, "/bar");
+ }
+
+ #[test]
+ fn test_url_builder() {
+ let url = Url::parse("https://example.com/foo").unwrap();
+ let req = RequestBuilder::from_url("GET", &url).unwrap().request();
+
+ assert_eq!(req.path, "/foo");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_query() {
+ let url = Url::parse("https://example.com/foo?foo=bar").unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, None);
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, "/foo?foo=bar");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_encodable_chars() {
+ let url = Url::parse("https://example.com/ñoo?foo=año").unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, None);
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, "/%C3%B1oo?foo=a%C3%B1o");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_empty_query() {
+ let url = Url::parse("https://example.com/foo?").unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, None);
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, "/foo?");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_bewit_alone() {
+ let url = Url::parse(&format!("https://example.com/foo?bewit={}", BEWIT_STR)).unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, Some(Bewit::from_str(BEWIT_STR).unwrap()));
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, "/foo"); // NOTE: strips the `?`
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_bewit_first() {
+ let url = Url::parse(&format!("https://example.com/foo?bewit={}&a=1", BEWIT_STR)).unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, Some(Bewit::from_str(BEWIT_STR).unwrap()));
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, "/foo?a=1");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_bewit_multiple() {
+ let url = Url::parse(&format!(
+ "https://example.com/foo?bewit={}&bewit={}",
+ BEWIT_STR, BEWIT_STR
+ ))
+ .unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ assert!(bldr.extract_bewit(&mut bewit).is_err());
+ assert_eq!(bewit, None);
+ }
+
+ #[test]
+ fn test_url_builder_with_bewit_invalid() {
+ let url = Url::parse("https://example.com/foo?bewit=1234").unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ assert!(bldr.extract_bewit(&mut bewit).is_err());
+ assert_eq!(bewit, None);
+ }
+
+ #[test]
+ fn test_url_builder_with_bewit_last() {
+ let url = Url::parse(&format!("https://example.com/foo?a=1&bewit={}", BEWIT_STR)).unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, Some(Bewit::from_str(BEWIT_STR).unwrap()));
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, "/foo?a=1");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_bewit_middle() {
+ let url = Url::parse(&format!(
+ "https://example.com/foo?a=1&bewit={}&b=2",
+ BEWIT_STR
+ ))
+ .unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, Some(Bewit::from_str(BEWIT_STR).unwrap()));
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, "/foo?a=1&b=2");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_bewit_percent_encoding() {
+ // Note that this *over*-encodes things. Perfectly legal, but the kind
+ // of thing that incautious libraries can sometimes fail to reproduce,
+ // causing Hawk validation failures
+ let url = Url::parse(&format!(
+ "https://example.com/foo?%66oo=1&bewit={}&%62ar=2",
+ BEWIT_STR
+ ))
+ .unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, Some(Bewit::from_str(BEWIT_STR).unwrap()));
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, "/foo?%66oo=1&%62ar=2");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_xxxbewit() {
+ // check that we're not doing a simple string search for "bewit=.."
+ let url = Url::parse(&format!(
+ "https://example.com/foo?a=1&xxxbewit={}&b=2",
+ BEWIT_STR
+ ))
+ .unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, None);
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, format!("/foo?a=1&xxxbewit={}&b=2", BEWIT_STR));
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_url_builder_with_username_password() {
+ let url = Url::parse("https://a:b@example.com/foo?x=y").unwrap();
+ let bldr = RequestBuilder::from_url("GET", &url).unwrap();
+
+ let mut bewit = Some(Bewit::from_str(INITIAL_BEWIT_STR).unwrap());
+ let bldr = bldr.extract_bewit(&mut bewit).unwrap();
+ assert_eq!(bewit, None);
+
+ let req = bldr.request();
+
+ assert_eq!(req.path, "/foo?x=y");
+ assert_eq!(req.host, "example.com");
+ assert_eq!(req.port, 443); // default for https
+ }
+
+ #[test]
+ fn test_make_header_full() {
+ let req = RequestBuilder::new("GET", "example.com", 443, "/foo").request();
+ let credentials = Credentials {
+ id: "me".to_string(),
+ key: Key::new(vec![99u8; 32], crate::SHA256).unwrap(),
+ };
+ let header = req
+ .make_header_full(&credentials, UNIX_EPOCH + Duration::new(1000, 100), "nonny")
+ .unwrap();
+ assert_eq!(
+ header,
+ Header {
+ id: Some("me".to_string()),
+ ts: Some(UNIX_EPOCH + Duration::new(1000, 100)),
+ nonce: Some("nonny".to_string()),
+ mac: Some(Mac::from(vec![
+ 122, 47, 2, 53, 195, 247, 185, 107, 133, 250, 61, 134, 200, 35, 118, 94, 48,
+ 175, 237, 108, 60, 71, 4, 2, 244, 66, 41, 172, 91, 7, 233, 140
+ ])),
+ ext: None,
+ hash: None,
+ app: None,
+ dlg: None,
+ }
+ );
+ }
+
+ #[test]
+ fn test_make_header_full_with_optional_fields() {
+ let hash = vec![0u8];
+ let req = RequestBuilder::new("GET", "example.com", 443, "/foo")
+ .hash(Some(&hash[..]))
+ .ext("ext")
+ .app("app")
+ .dlg("dlg")
+ .request();
+ let credentials = Credentials {
+ id: "me".to_string(),
+ key: Key::new(vec![99u8; 32], crate::SHA256).unwrap(),
+ };
+ let header = req
+ .make_header_full(&credentials, UNIX_EPOCH + Duration::new(1000, 100), "nonny")
+ .unwrap();
+ assert_eq!(
+ header,
+ Header {
+ id: Some("me".to_string()),
+ ts: Some(UNIX_EPOCH + Duration::new(1000, 100)),
+ nonce: Some("nonny".to_string()),
+ mac: Some(Mac::from(vec![
+ 72, 123, 243, 214, 145, 81, 129, 54, 183, 90, 22, 136, 192, 146, 208, 53, 216,
+ 138, 145, 94, 175, 204, 217, 8, 77, 16, 202, 50, 10, 144, 133, 162
+ ])),
+ ext: Some("ext".to_string()),
+ hash: Some(hash.clone()),
+ app: Some("app".to_string()),
+ dlg: Some("dlg".to_string()),
+ }
+ );
+ }
+
+ #[test]
+ fn test_validate_matches_generated() {
+ let req = RequestBuilder::new("GET", "example.com", 443, "/foo").request();
+ let credentials = Credentials {
+ id: "me".to_string(),
+ key: Key::new(vec![99u8; 32], crate::SHA256).unwrap(),
+ };
+ let header = req
+ .make_header_full(&credentials, SystemTime::now(), "nonny")
+ .unwrap();
+ assert!(req.validate_header(&header, &credentials.key, Duration::from_secs(1 * 60)));
+ }
+
+ // Well, close enough.
+ const ONE_YEAR_IN_SECS: u64 = 365 * 24 * 60 * 60;
+
+ #[test]
+ fn test_validate_real_request() {
+ let header = Header::from_str(REAL_HEADER).unwrap();
+ let credentials = Credentials {
+ id: "me".to_string(),
+ key: Key::new("tok", crate::SHA256).unwrap(),
+ };
+ let req =
+ RequestBuilder::new("GET", "pulse.taskcluster.net", 443, "/v1/namespaces").request();
+ // allow 1000 years skew, since this was a real request that
+ // happened back in 2017, when life was simple and carefree
+ assert!(req.validate_header(
+ &header,
+ &credentials.key,
+ Duration::from_secs(1000 * ONE_YEAR_IN_SECS)
+ ));
+ }
+
+ #[test]
+ fn test_validate_real_request_bad_creds() {
+ let header = Header::from_str(REAL_HEADER).unwrap();
+ let credentials = Credentials {
+ id: "me".to_string(),
+ key: Key::new("WRONG", crate::SHA256).unwrap(),
+ };
+ let req =
+ RequestBuilder::new("GET", "pulse.taskcluster.net", 443, "/v1/namespaces").request();
+ assert!(!req.validate_header(
+ &header,
+ &credentials.key,
+ Duration::from_secs(1000 * ONE_YEAR_IN_SECS)
+ ));
+ }
+
+ #[test]
+ fn test_validate_real_request_bad_req_info() {
+ let header = Header::from_str(REAL_HEADER).unwrap();
+ let credentials = Credentials {
+ id: "me".to_string(),
+ key: Key::new("tok", crate::SHA256).unwrap(),
+ };
+ let req = RequestBuilder::new("GET", "pulse.taskcluster.net", 443, "WRONG PATH").request();
+ assert!(!req.validate_header(
+ &header,
+ &credentials.key,
+ Duration::from_secs(1000 * ONE_YEAR_IN_SECS)
+ ));
+ }
+
+ fn make_header_without_hash() -> Header {
+ Header::new(
+ Some("dh37fgj492je"),
+ Some(UNIX_EPOCH + Duration::new(1353832234, 0)),
+ Some("j4h3g2"),
+ Some(Mac::from(vec![
+ 161, 105, 122, 110, 248, 62, 129, 193, 148, 206, 239, 193, 219, 46, 137, 221, 51,
+ 170, 135, 114, 81, 68, 145, 182, 15, 165, 145, 168, 114, 237, 52, 35,
+ ])),
+ None,
+ None,
+ None,
+ None,
+ )
+ .unwrap()
+ }
+
+ fn make_header_with_hash() -> Header {
+ Header::new(
+ Some("dh37fgj492je"),
+ Some(UNIX_EPOCH + Duration::new(1353832234, 0)),
+ Some("j4h3g2"),
+ Some(Mac::from(vec![
+ 189, 53, 155, 244, 203, 150, 255, 238, 135, 144, 186, 93, 6, 189, 184, 21, 150,
+ 210, 226, 61, 93, 154, 17, 218, 142, 250, 254, 193, 123, 132, 131, 195,
+ ])),
+ None,
+ Some(vec![1, 2, 3, 4]),
+ None,
+ None,
+ )
+ .unwrap()
+ }
+
+ #[test]
+ fn test_validate_no_hash() {
+ let header = make_header_without_hash();
+ let req = RequestBuilder::new("", "", 0, "").request();
+ assert!(req.validate_header(
+ &header,
+ &Key::new("tok", crate::SHA256).unwrap(),
+ Duration::from_secs(1000 * ONE_YEAR_IN_SECS)
+ ));
+ }
+
+ #[test]
+ fn test_validate_hash_in_header() {
+ let header = make_header_with_hash();
+ let req = RequestBuilder::new("", "", 0, "").request();
+ assert!(req.validate_header(
+ &header,
+ &Key::new("tok", crate::SHA256).unwrap(),
+ Duration::from_secs(1000 * ONE_YEAR_IN_SECS)
+ ));
+ }
+
+ #[test]
+ fn test_validate_hash_required_but_not_given() {
+ let header = make_header_without_hash();
+ let hash = vec![1, 2, 3, 4];
+ let req = RequestBuilder::new("", "", 0, "")
+ .hash(Some(&hash[..]))
+ .request();
+ assert!(!req.validate_header(
+ &header,
+ &Key::new("tok", crate::SHA256).unwrap(),
+ Duration::from_secs(1000 * ONE_YEAR_IN_SECS)
+ ));
+ }
+
+ #[test]
+ fn test_validate_hash_validated() {
+ let header = make_header_with_hash();
+ let hash = vec![1, 2, 3, 4];
+ let req = RequestBuilder::new("", "", 0, "")
+ .hash(Some(&hash[..]))
+ .request();
+ assert!(req.validate_header(
+ &header,
+ &Key::new("tok", crate::SHA256).unwrap(),
+ Duration::from_secs(1000 * ONE_YEAR_IN_SECS)
+ ));
+
+ // ..but supplying the wrong hash will cause validation to fail
+ let hash = vec![99, 99, 99, 99];
+ let req = RequestBuilder::new("", "", 0, "")
+ .hash(Some(&hash[..]))
+ .request();
+ assert!(!req.validate_header(
+ &header,
+ &Key::new("tok", crate::SHA256).unwrap(),
+ Duration::from_secs(1000 * ONE_YEAR_IN_SECS)
+ ));
+ }
+
+ fn round_trip_bewit(req: Request, ts: SystemTime, expected: bool) {
+ let credentials = Credentials {
+ id: "me".to_string(),
+ key: Key::new("tok", crate::SHA256).unwrap(),
+ };
+
+ let bewit = req.make_bewit(&credentials, ts).unwrap();
+
+ // convert to a string and back
+ let bewit = bewit.to_str();
+ let bewit = Bewit::from_str(&bewit).unwrap();
+
+ // and validate it maches the original request
+ assert_eq!(req.validate_bewit(&bewit, &credentials.key), expected);
+ }
+
+ #[test]
+ fn test_validate_bewit() {
+ let req = RequestBuilder::new("GET", "foo.com", 443, "/x/y/z").request();
+ round_trip_bewit(req, SystemTime::now() + Duration::from_secs(10 * 60), true);
+ }
+
+ #[test]
+ fn test_validate_bewit_ext() {
+ let req = RequestBuilder::new("GET", "foo.com", 443, "/x/y/z")
+ .ext("abcd")
+ .request();
+ round_trip_bewit(req, SystemTime::now() + Duration::from_secs(10 * 60), true);
+ }
+
+ #[test]
+ fn test_validate_bewit_expired() {
+ let req = RequestBuilder::new("GET", "foo.com", 443, "/x/y/z").request();
+ round_trip_bewit(req, SystemTime::now() - Duration::from_secs(10 * 60), false);
+ }
+}