use crate::b64;
use crate::error::*;
use crate::mac::Mac;
use base64::Engine;
use std::fmt;
use std::str::FromStr;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Clone, PartialEq, Debug)]
pub struct Header {
pub id: Option<String>,
pub ts: Option<SystemTime>,
pub nonce: Option<String>,
pub mac: Option<Mac>,
pub ext: Option<String>,
pub hash: Option<Vec<u8>>,
pub app: Option<String>,
pub dlg: Option<String>,
}
impl Header {
pub fn new<S>(
id: Option<S>,
ts: Option<SystemTime>,
nonce: Option<S>,
mac: Option<Mac>,
ext: Option<S>,
hash: Option<Vec<u8>>,
app: Option<S>,
dlg: Option<S>,
) -> Result<Header>
where
S: Into<String>,
{
Ok(Header {
id: Header::check_component(id)?,
ts,
nonce: Header::check_component(nonce)?,
mac,
ext: Header::check_component(ext)?,
hash,
app: Header::check_component(app)?,
dlg: Header::check_component(dlg)?,
})
}
fn check_component<S>(value: Option<S>) -> Result<Option<String>>
where
S: Into<String>,
{
if let Some(value) = value {
let value = value.into();
if value.contains('\"') {
return Err(Error::HeaderParseError(
"Hawk headers cannot contain `\\`".into(),
));
}
Ok(Some(value))
} else {
Ok(None)
}
}
pub fn fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut sep = "";
if let Some(ref id) = self.id {
write!(f, "{sep}id=\"{id}\"")?;
sep = ", ";
}
if let Some(ref ts) = self.ts {
write!(
f,
"{}ts=\"{}\"",
sep,
ts.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()
)?;
sep = ", ";
}
if let Some(ref nonce) = self.nonce {
write!(f, "{sep}nonce=\"{nonce}\"")?;
sep = ", ";
}
if let Some(ref mac) = self.mac {
write!(f, "{}mac=\"{}\"", sep, b64::STANDARD_ENGINE.encode(mac))?;
sep = ", ";
}
if let Some(ref ext) = self.ext {
write!(f, "{sep}ext=\"{ext}\"")?;
sep = ", ";
}
if let Some(ref hash) = self.hash {
write!(f, "{}hash=\"{}\"", sep, b64::STANDARD_ENGINE.encode(hash))?;
sep = ", ";
}
if let Some(ref app) = self.app {
write!(f, "{sep}app=\"{app}\"")?;
sep = ", ";
}
if let Some(ref dlg) = self.dlg {
write!(f, "{sep}dlg=\"{dlg}\"")?;
}
Ok(())
}
}
impl fmt::Display for Header {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.fmt_header(f)
}
}
impl FromStr for Header {
type Err = Error;
fn from_str(s: &str) -> Result<Header> {
let mut p = s;
let mut id: Option<&str> = None;
let mut ts: Option<SystemTime> = None;
let mut nonce: Option<&str> = None;
let mut mac: Option<Vec<u8>> = None;
let mut hash: Option<Vec<u8>> = None;
let mut ext: Option<&str> = None;
let mut app: Option<&str> = None;
let mut dlg: Option<&str> = None;
while !p.is_empty() {
p = p.trim_start_matches(|c| c == ',' || char::is_whitespace(c));
let assign_end = p
.find('=')
.ok_or_else(|| Error::HeaderParseError("Expected '='".into()))?;
let attr = &p[..assign_end].trim();
if p.len() < assign_end + 1 {
return Err(Error::HeaderParseError(
"Missing right hand side of =".into(),
));
}
p = p[assign_end + 1..].trim_start();
if !p.starts_with('\"') {
return Err(Error::HeaderParseError("Expected opening quote".into()));
}
p = &p[1..];
let end = p.find('\"');
let val_end =
end.ok_or_else(|| Error::HeaderParseError("Expected closing quote".into()))?;
let val = &p[..val_end];
match *attr {
"id" => id = Some(val),
"ts" => {
let epoch = u64::from_str(val)
.map_err(|_| Error::HeaderParseError("Error parsing `ts` field".into()))?;
ts = Some(UNIX_EPOCH + Duration::new(epoch, 0));
}
"mac" => {
mac = Some(b64::STANDARD_ENGINE.decode(val).map_err(|_| {
Error::HeaderParseError("Error parsing `mac` field".into())
})?);
}
"nonce" => nonce = Some(val),
"ext" => ext = Some(val),
"hash" => {
hash = Some(b64::STANDARD_ENGINE.decode(val).map_err(|_| {
Error::HeaderParseError("Error parsing `hash` field".into())
})?);
}
"app" => app = Some(val),
"dlg" => dlg = Some(val),
_ => {
return Err(Error::HeaderParseError(format!(
"Invalid Hawk field {}",
*attr
)))
}
};
if p.len() < val_end + 1 {
break;
}
p = p[val_end + 1..].trim_start();
}
Ok(Header {
id: id.map(|id| id.to_string()),
ts,
nonce: nonce.map(|nonce| nonce.to_string()),
mac: mac.map(Mac::from),
ext: ext.map(|ext| ext.to_string()),
hash,
app: app.map(|app| app.to_string()),
dlg: dlg.map(|dlg| dlg.to_string()),
})
}
}
#[cfg(test)]
mod test {
use super::Header;
use crate::mac::Mac;
use std::str::FromStr;
use std::time::{Duration, UNIX_EPOCH};
#[test]
fn illegal_id() {
assert!(Header::new(
Some("ab\"cdef"),
Some(UNIX_EPOCH + Duration::new(1234, 0)),
Some("nonce"),
Some(Mac::from(vec![])),
Some("ext"),
None,
None,
None
)
.is_err());
}
#[test]
fn illegal_nonce() {
assert!(Header::new(
Some("abcdef"),
Some(UNIX_EPOCH + Duration::new(1234, 0)),
Some("no\"nce"),
Some(Mac::from(vec![])),
Some("ext"),
None,
None,
None
)
.is_err());
}
#[test]
fn illegal_ext() {
assert!(Header::new(
Some("abcdef"),
Some(UNIX_EPOCH + Duration::new(1234, 0)),
Some("nonce"),
Some(Mac::from(vec![])),
Some("ex\"t"),
None,
None,
None
)
.is_err());
}
#[test]
fn illegal_app() {
assert!(Header::new(
Some("abcdef"),
Some(UNIX_EPOCH + Duration::new(1234, 0)),
Some("nonce"),
Some(Mac::from(vec![])),
None,
None,
Some("a\"pp"),
None
)
.is_err());
}
#[test]
fn illegal_dlg() {
assert!(Header::new(
Some("abcdef"),
Some(UNIX_EPOCH + Duration::new(1234, 0)),
Some("nonce"),
Some(Mac::from(vec![])),
None,
None,
None,
Some("d\"lg")
)
.is_err());
}
#[test]
fn from_str() {
let s = Header::from_str(
"id=\"dh37fgj492je\", ts=\"1353832234\", \
nonce=\"j4h3g2\", ext=\"some-app-ext-data\", \
mac=\"6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=\", \
hash=\"6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=\", \
app=\"my-app\", dlg=\"my-authority\"",
)
.unwrap();
assert!(s.id == Some("dh37fgj492je".to_string()));
assert!(s.ts == Some(UNIX_EPOCH + Duration::new(1353832234, 0)));
assert!(s.nonce == Some("j4h3g2".to_string()));
assert!(
s.mac
== Some(Mac::from(vec![
233, 30, 43, 87, 152, 132, 248, 211, 232, 202, 111, 150, 194, 55, 135, 206, 48,
6, 93, 75, 75, 52, 140, 102, 163, 91, 233, 50, 135, 233, 44, 1
]))
);
assert!(s.ext == Some("some-app-ext-data".to_string()));
assert!(s.app == Some("my-app".to_string()));
assert!(s.dlg == Some("my-authority".to_string()));
}
#[test]
fn from_str_invalid_mac() {
let r = Header::from_str(
"id=\"dh37fgj492je\", ts=\"1353832234\", \
nonce=\"j4h3g2\", ext=\"some-app-ext-data\", \
mac=\"6!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!AE=\", \
app=\"my-app\", dlg=\"my-authority\"",
);
assert!(r.is_err());
}
#[test]
fn from_str_no_field() {
let s = Header::from_str("").unwrap();
assert!(s.id.is_none());
assert!(s.ts.is_none());
assert!(s.nonce.is_none());
assert!(s.mac.is_none());
assert!(s.ext.is_none());
assert!(s.app.is_none());
assert!(s.dlg.is_none());
}
#[test]
fn from_str_few_field() {
let s = Header::from_str(
"id=\"xyz\", ts=\"1353832234\", \
nonce=\"abc\", \
mac=\"6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=\"",
)
.unwrap();
assert!(s.id == Some("xyz".to_string()));
assert!(s.ts == Some(UNIX_EPOCH + Duration::new(1353832234, 0)));
assert!(s.nonce == Some("abc".to_string()));
assert!(
s.mac
== Some(Mac::from(vec![
233, 30, 43, 87, 152, 132, 248, 211, 232, 202, 111, 150, 194, 55, 135, 206, 48,
6, 93, 75, 75, 52, 140, 102, 163, 91, 233, 50, 135, 233, 44, 1
]))
);
assert!(s.ext.is_none());
assert!(s.app.is_none());
assert!(s.dlg.is_none());
}
#[test]
fn from_str_messy() {
let s = Header::from_str(
", id = \"dh37fgj492je\", ts=\"1353832234\", \
nonce=\"j4h3g2\" , , ext=\"some-app-ext-data\", \
mac=\"6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=\"",
)
.unwrap();
assert!(s.id == Some("dh37fgj492je".to_string()));
assert!(s.ts == Some(UNIX_EPOCH + Duration::new(1353832234, 0)));
assert!(s.nonce == Some("j4h3g2".to_string()));
assert!(
s.mac
== Some(Mac::from(vec![
233, 30, 43, 87, 152, 132, 248, 211, 232, 202, 111, 150, 194, 55, 135, 206, 48,
6, 93, 75, 75, 52, 140, 102, 163, 91, 233, 50, 135, 233, 44, 1
]))
);
assert!(s.ext == Some("some-app-ext-data".to_string()));
assert!(s.app.is_none());
assert!(s.dlg.is_none());
}
#[test]
fn to_str_no_fields() {
let s = Header::new::<String>(None, None, None, None, None, None, None, None).unwrap();
let formatted = format!("{s}");
println!("got: {formatted}");
assert!(formatted.is_empty())
}
#[test]
fn to_str_few_fields() {
let s = Header::new(
Some("dh37fgj492je"),
Some(UNIX_EPOCH + Duration::new(1353832234, 0)),
Some("j4h3g2"),
Some(Mac::from(vec![
8, 35, 182, 149, 42, 111, 33, 192, 19, 22, 94, 43, 118, 176, 65, 69, 86, 4, 156,
184, 85, 107, 249, 242, 172, 200, 66, 209, 57, 63, 38, 83,
])),
None,
None,
None,
None,
)
.unwrap();
let formatted = format!("{s}");
println!("got: {formatted}");
assert!(
formatted
== "id=\"dh37fgj492je\", ts=\"1353832234\", nonce=\"j4h3g2\", \
mac=\"CCO2lSpvIcATFl4rdrBBRVYEnLhVa/nyrMhC0Tk/JlM=\""
)
}
#[test]
fn to_str_maximal() {
let s = Header::new(
Some("dh37fgj492je"),
Some(UNIX_EPOCH + Duration::new(1353832234, 0)),
Some("j4h3g2"),
Some(Mac::from(vec![
8, 35, 182, 149, 42, 111, 33, 192, 19, 22, 94, 43, 118, 176, 65, 69, 86, 4, 156,
184, 85, 107, 249, 242, 172, 200, 66, 209, 57, 63, 38, 83,
])),
Some("my-ext-value"),
Some(vec![1, 2, 3, 4]),
Some("my-app"),
Some("my-dlg"),
)
.unwrap();
let formatted = format!("{s}");
println!("got: {formatted}");
assert!(
formatted
== "id=\"dh37fgj492je\", ts=\"1353832234\", nonce=\"j4h3g2\", \
mac=\"CCO2lSpvIcATFl4rdrBBRVYEnLhVa/nyrMhC0Tk/JlM=\", ext=\"my-ext-value\", \
hash=\"AQIDBA==\", app=\"my-app\", dlg=\"my-dlg\""
)
}
#[test]
fn round_trip() {
let s = Header::new(
Some("dh37fgj492je"),
Some(UNIX_EPOCH + Duration::new(1353832234, 0)),
Some("j4h3g2"),
Some(Mac::from(vec![
8, 35, 182, 149, 42, 111, 33, 192, 19, 22, 94, 43, 118, 176, 65, 69, 86, 4, 156,
184, 85, 107, 249, 242, 172, 200, 66, 209, 57, 63, 38, 83,
])),
Some("my-ext-value"),
Some(vec![1, 2, 3, 4]),
Some("my-app"),
Some("my-dlg"),
)
.unwrap();
let formatted = format!("{s}");
println!("got: {s}");
let s2 = Header::from_str(&formatted).unwrap();
assert!(s2 == s);
}
}