usecrate::b64; usecrate::bewit::Bewit; usecrate::credentials::{Credentials, Key}; usecrate::error::*; usecrate::header::Header; usecrate::mac::{Mac, MacType}; usecrate::response::ResponseBuilder; use base64::Engine; 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)] pubstruct 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. pubfn 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. pubfn 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), self.ext.map(|v| v.to_string()), self.hash.map(|v| v.to_vec()), self.app.map(|v| v.to_string()), self.dlg.map(|v| 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. pubfn 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. pubfn 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! pubfn 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"); returnfalse;
}
}; let nonce = match header.nonce {
Some(ref nonce) => nonce,
None => {
debug!("missing nonce from header"); returnfalse;
}
}; let header_mac = match header.mac {
Some(ref mac) => mac,
None => {
debug!("missing mac from header"); returnfalse;
}
}; let header_hash = header.hash.as_ref().map(|hash| &hash[..]); let header_ext = header.ext.as_ref().map(|ext| &ext[..]);
// 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"); returnfalse;
}
}
Err(e) => {
debug!("unexpected mac error: {:?}", e); returnfalse;
}
};
// ..then the hashes iflet Some(local_hash) = self.hash { iflet Some(server_hash) = header_hash { if local_hash != server_hash {
debug!("server hash doesn't match header"); returnfalse;
}
} else {
debug!("missing hash from header"); returnfalse;
}
}
// ..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
); returnfalse;
}
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. pubfn 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(_) => { returnfalse;
}
};
if bewit.mac() != &calculated_mac { returnfalse;
}
let now = SystemTime::now(); if bewit.exp() < now { returnfalse;
}
true
}
/// Get a Response instance for a response to this request. This is a convenience /// wrapper around `Response::from_request_header`. pubfn 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(),
)
}
}
impl<'a> RequestBuilder<'a> { /// Create a new request with the given method, host, port, and path. pubfn new(method: &'a str, host: &'a str, port: u16, path: & style='color:blue'>'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. pubfn 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. pubfn method(mutself, method: &'a str) -> Self { self.0.method = method; self
}
/// Set the URL path for the request. pubfn path(mutself, path: &'a str) -> Self { self.0.path = Cow::Borrowed(path); self
}
/// Set the URL hostname for the request pubfn host(mutself, host: &'a str) -> Self { self.0.host = host; self
}
/// Set the URL port for the request pubfn port(mutself, port: u16) -> Self { self.0.port = port; self
}
/// Set the hostname, port, and path for the request, from a string URL. pubfn 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 pubfn 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 pubfn 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 pubfn 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 pubfn dlg<S: Into<Option<&'a str>>>(mut self, dlg: S) -> Self { self.0.dlg = dlg.into(); self
}
/// Get the request from this builder pubfn 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 pubfn extract_bewit(mutself, bewit: &mut Option<Bewit<'a>>) -> Result<Self> { const PREFIX: &str = "bewit=";
*bewit = None;
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("&"))
} 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)
} elseif bewit_components.is_empty() {
Ok(self)
} else {
Err(InvalidBewit::Multiple.into())
}
} else {
Ok(self)
}
}
fn parse_url(url: &'a Url) -> Result<(&'a str, u16, &'color:blue'>'a str)> { let host = url
.host_str()
.ok_or_else(|| Error::InvalidUrl(format!("url {url} has no host")))?; let port = url
.port_or_known_default()
.ok_or_else(|| Error::InvalidUrl(format!("url {url} has no port")))?; 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> { letmut bytes = vec![0u8; bytes]; crate::crypto::rand_bytes(&mut bytes)?;
Ok(b64::BEWIT_ENGINE.encode(&bytes))
}
#[cfg(all(test, any(feature = "use_ring", feature = "use_openssl")))] mod test { usesuper::*; usecrate::credentials::{Credentials, Key}; usecrate::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: &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_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={BEWIT_STR}&%62ar=2"
))
.unwrap(); let bldr = RequestBuilder::from_url("GET", &url).unwrap();
#[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={BEWIT_STR}&b=2"
))
.unwrap(); let bldr = RequestBuilder::from_url("GET", &url).unwrap();
#[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)
));
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.