1 use crate::error::*; 2 use crate::mac::Mac; 3 use base64::display::Base64Display; 4 use std::fmt; 5 use std::str::FromStr; 6 use std::time::{Duration, SystemTime, UNIX_EPOCH}; 7 /// Representation of a Hawk `Authorization` header value (the part following "Hawk "). 8 /// 9 /// Headers can be derived from strings using the `FromStr` trait, and formatted into a 10 /// string using the `fmt_header` method. 11 /// 12 /// All fields are optional, although for specific purposes some fields must be present. 13 #[derive(Clone, PartialEq, Debug)] 14 pub struct Header { 15 pub id: Option<String>, 16 pub ts: Option<SystemTime>, 17 pub nonce: Option<String>, 18 pub mac: Option<Mac>, 19 pub ext: Option<String>, 20 pub hash: Option<Vec<u8>>, 21 pub app: Option<String>, 22 pub dlg: Option<String>, 23 } 24 25 impl Header { 26 /// Create a new Header with the full set of Hawk fields. 27 /// 28 /// This is a low-level function. Headers are more often created from Requests or Responses. 29 /// 30 /// Note that none of the string-formatted header components can contain the character `\"`. 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>,31 pub fn new<S>( 32 id: Option<S>, 33 ts: Option<SystemTime>, 34 nonce: Option<S>, 35 mac: Option<Mac>, 36 ext: Option<S>, 37 hash: Option<Vec<u8>>, 38 app: Option<S>, 39 dlg: Option<S>, 40 ) -> Result<Header> 41 where 42 S: Into<String>, 43 { 44 Ok(Header { 45 id: Header::check_component(id)?, 46 ts, 47 nonce: Header::check_component(nonce)?, 48 mac, 49 ext: Header::check_component(ext)?, 50 hash, 51 app: Header::check_component(app)?, 52 dlg: Header::check_component(dlg)?, 53 }) 54 } 55 56 /// Check a header component for validity. check_component<S>(value: Option<S>) -> Result<Option<String>> where S: Into<String>,57 fn check_component<S>(value: Option<S>) -> Result<Option<String>> 58 where 59 S: Into<String>, 60 { 61 if let Some(value) = value { 62 let value = value.into(); 63 if value.contains('\"') { 64 return Err(Error::HeaderParseError( 65 "Hawk headers cannot contain `\\`".into(), 66 )); 67 } 68 Ok(Some(value)) 69 } else { 70 Ok(None) 71 } 72 } 73 74 /// Format the header for transmission in an Authorization header, omitting the `"Hawk "` 75 /// prefix. fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result76 pub fn fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result { 77 let mut sep = ""; 78 if let Some(ref id) = self.id { 79 write!(f, "{}id=\"{}\"", sep, id)?; 80 sep = ", "; 81 } 82 if let Some(ref ts) = self.ts { 83 write!( 84 f, 85 "{}ts=\"{}\"", 86 sep, 87 ts.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs() 88 )?; 89 sep = ", "; 90 } 91 if let Some(ref nonce) = self.nonce { 92 write!(f, "{}nonce=\"{}\"", sep, nonce)?; 93 sep = ", "; 94 } 95 if let Some(ref mac) = self.mac { 96 write!( 97 f, 98 "{}mac=\"{}\"", 99 sep, 100 Base64Display::with_config(mac, base64::STANDARD) 101 )?; 102 sep = ", "; 103 } 104 if let Some(ref ext) = self.ext { 105 write!(f, "{}ext=\"{}\"", sep, ext)?; 106 sep = ", "; 107 } 108 if let Some(ref hash) = self.hash { 109 write!( 110 f, 111 "{}hash=\"{}\"", 112 sep, 113 Base64Display::with_config(hash, base64::STANDARD) 114 )?; 115 sep = ", "; 116 } 117 if let Some(ref app) = self.app { 118 write!(f, "{}app=\"{}\"", sep, app)?; 119 sep = ", "; 120 } 121 if let Some(ref dlg) = self.dlg { 122 write!(f, "{}dlg=\"{}\"", sep, dlg)?; 123 } 124 Ok(()) 125 } 126 } 127 128 impl fmt::Display for Header { fmt(&self, f: &mut fmt::Formatter) -> fmt::Result129 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 130 self.fmt_header(f) 131 } 132 } 133 134 impl FromStr for Header { 135 type Err = Error; from_str(s: &str) -> Result<Header>136 fn from_str(s: &str) -> Result<Header> { 137 let mut p = &s[..]; 138 139 // Required attributes 140 let mut id: Option<&str> = None; 141 let mut ts: Option<SystemTime> = None; 142 let mut nonce: Option<&str> = None; 143 let mut mac: Option<Vec<u8>> = None; 144 // Optional attributes 145 let mut hash: Option<Vec<u8>> = None; 146 let mut ext: Option<&str> = None; 147 let mut app: Option<&str> = None; 148 let mut dlg: Option<&str> = None; 149 150 while !p.is_empty() { 151 // Skip whitespace and commas used as separators 152 p = p.trim_start_matches(|c| c == ',' || char::is_whitespace(c)); 153 // Find first '=' which delimits attribute name from value 154 let assign_end = p 155 .find('=') 156 .ok_or_else(|| Error::HeaderParseError("Expected '='".into()))?; 157 let attr = &p[..assign_end].trim(); 158 if p.len() < assign_end + 1 { 159 return Err(Error::HeaderParseError( 160 "Missing right hand side of =".into(), 161 )); 162 } 163 p = (&p[assign_end + 1..]).trim_start(); 164 if !p.starts_with('\"') { 165 return Err(Error::HeaderParseError("Expected opening quote".into())); 166 } 167 p = &p[1..]; 168 // We have poor RFC 7235 compliance here as we ought to support backslash 169 // escaped characters, but hawk doesn't allow this we won't either. All 170 // strings must be surrounded by ".." and contain no such characters. 171 let end = p.find('\"'); 172 let val_end = 173 end.ok_or_else(|| Error::HeaderParseError("Expected closing quote".into()))?; 174 let val = &p[..val_end]; 175 match *attr { 176 "id" => id = Some(val), 177 "ts" => { 178 let epoch = u64::from_str(val) 179 .map_err(|_| Error::HeaderParseError("Error parsing `ts` field".into()))?; 180 ts = Some(UNIX_EPOCH + Duration::new(epoch, 0)); 181 } 182 "mac" => { 183 mac = Some(base64::decode(val).map_err(|_| { 184 Error::HeaderParseError("Error parsing `mac` field".into()) 185 })?); 186 } 187 "nonce" => nonce = Some(val), 188 "ext" => ext = Some(val), 189 "hash" => { 190 hash = Some(base64::decode(val).map_err(|_| { 191 Error::HeaderParseError("Error parsing `hash` field".into()) 192 })?); 193 } 194 "app" => app = Some(val), 195 "dlg" => dlg = Some(val), 196 _ => { 197 return Err(Error::HeaderParseError(format!( 198 "Invalid Hawk field {}", 199 *attr 200 ))) 201 } 202 }; 203 // Break if we are at end of string, otherwise skip separator 204 if p.len() < val_end + 1 { 205 break; 206 } 207 p = p[val_end + 1..].trim_start(); 208 } 209 210 Ok(Header { 211 id: match id { 212 Some(id) => Some(id.to_string()), 213 None => None, 214 }, 215 ts, 216 nonce: match nonce { 217 Some(nonce) => Some(nonce.to_string()), 218 None => None, 219 }, 220 mac: match mac { 221 Some(mac) => Some(Mac::from(mac)), 222 None => None, 223 }, 224 ext: match ext { 225 Some(ext) => Some(ext.to_string()), 226 None => None, 227 }, 228 hash, 229 app: match app { 230 Some(app) => Some(app.to_string()), 231 None => None, 232 }, 233 dlg: match dlg { 234 Some(dlg) => Some(dlg.to_string()), 235 None => None, 236 }, 237 }) 238 } 239 } 240 241 #[cfg(test)] 242 mod test { 243 use super::Header; 244 use crate::mac::Mac; 245 use std::str::FromStr; 246 use std::time::{Duration, UNIX_EPOCH}; 247 248 #[test] illegal_id()249 fn illegal_id() { 250 assert!(Header::new( 251 Some("ab\"cdef"), 252 Some(UNIX_EPOCH + Duration::new(1234, 0)), 253 Some("nonce"), 254 Some(Mac::from(vec![])), 255 Some("ext"), 256 None, 257 None, 258 None 259 ) 260 .is_err()); 261 } 262 263 #[test] illegal_nonce()264 fn illegal_nonce() { 265 assert!(Header::new( 266 Some("abcdef"), 267 Some(UNIX_EPOCH + Duration::new(1234, 0)), 268 Some("no\"nce"), 269 Some(Mac::from(vec![])), 270 Some("ext"), 271 None, 272 None, 273 None 274 ) 275 .is_err()); 276 } 277 278 #[test] illegal_ext()279 fn illegal_ext() { 280 assert!(Header::new( 281 Some("abcdef"), 282 Some(UNIX_EPOCH + Duration::new(1234, 0)), 283 Some("nonce"), 284 Some(Mac::from(vec![])), 285 Some("ex\"t"), 286 None, 287 None, 288 None 289 ) 290 .is_err()); 291 } 292 293 #[test] illegal_app()294 fn illegal_app() { 295 assert!(Header::new( 296 Some("abcdef"), 297 Some(UNIX_EPOCH + Duration::new(1234, 0)), 298 Some("nonce"), 299 Some(Mac::from(vec![])), 300 None, 301 None, 302 Some("a\"pp"), 303 None 304 ) 305 .is_err()); 306 } 307 308 #[test] illegal_dlg()309 fn illegal_dlg() { 310 assert!(Header::new( 311 Some("abcdef"), 312 Some(UNIX_EPOCH + Duration::new(1234, 0)), 313 Some("nonce"), 314 Some(Mac::from(vec![])), 315 None, 316 None, 317 None, 318 Some("d\"lg") 319 ) 320 .is_err()); 321 } 322 323 #[test] from_str()324 fn from_str() { 325 let s = Header::from_str( 326 "id=\"dh37fgj492je\", ts=\"1353832234\", \ 327 nonce=\"j4h3g2\", ext=\"some-app-ext-data\", \ 328 mac=\"6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=\", \ 329 hash=\"6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=\", \ 330 app=\"my-app\", dlg=\"my-authority\"", 331 ) 332 .unwrap(); 333 assert!(s.id == Some("dh37fgj492je".to_string())); 334 assert!(s.ts == Some(UNIX_EPOCH + Duration::new(1353832234, 0))); 335 assert!(s.nonce == Some("j4h3g2".to_string())); 336 assert!( 337 s.mac 338 == Some(Mac::from(vec![ 339 233, 30, 43, 87, 152, 132, 248, 211, 232, 202, 111, 150, 194, 55, 135, 206, 48, 340 6, 93, 75, 75, 52, 140, 102, 163, 91, 233, 50, 135, 233, 44, 1 341 ])) 342 ); 343 assert!(s.ext == Some("some-app-ext-data".to_string())); 344 assert!(s.app == Some("my-app".to_string())); 345 assert!(s.dlg == Some("my-authority".to_string())); 346 } 347 348 #[test] from_str_invalid_mac()349 fn from_str_invalid_mac() { 350 let r = Header::from_str( 351 "id=\"dh37fgj492je\", ts=\"1353832234\", \ 352 nonce=\"j4h3g2\", ext=\"some-app-ext-data\", \ 353 mac=\"6!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!AE=\", \ 354 app=\"my-app\", dlg=\"my-authority\"", 355 ); 356 assert!(r.is_err()); 357 } 358 359 #[test] from_str_no_field()360 fn from_str_no_field() { 361 let s = Header::from_str("").unwrap(); 362 assert!(s.id == None); 363 assert!(s.ts == None); 364 assert!(s.nonce == None); 365 assert!(s.mac == None); 366 assert!(s.ext == None); 367 assert!(s.app == None); 368 assert!(s.dlg == None); 369 } 370 371 #[test] from_str_few_field()372 fn from_str_few_field() { 373 let s = Header::from_str( 374 "id=\"xyz\", ts=\"1353832234\", \ 375 nonce=\"abc\", \ 376 mac=\"6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=\"", 377 ) 378 .unwrap(); 379 assert!(s.id == Some("xyz".to_string())); 380 assert!(s.ts == Some(UNIX_EPOCH + Duration::new(1353832234, 0))); 381 assert!(s.nonce == Some("abc".to_string())); 382 assert!( 383 s.mac 384 == Some(Mac::from(vec![ 385 233, 30, 43, 87, 152, 132, 248, 211, 232, 202, 111, 150, 194, 55, 135, 206, 48, 386 6, 93, 75, 75, 52, 140, 102, 163, 91, 233, 50, 135, 233, 44, 1 387 ])) 388 ); 389 assert!(s.ext == None); 390 assert!(s.app == None); 391 assert!(s.dlg == None); 392 } 393 394 #[test] from_str_messy()395 fn from_str_messy() { 396 let s = Header::from_str( 397 ", id = \"dh37fgj492je\", ts=\"1353832234\", \ 398 nonce=\"j4h3g2\" , , ext=\"some-app-ext-data\", \ 399 mac=\"6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=\"", 400 ) 401 .unwrap(); 402 assert!(s.id == Some("dh37fgj492je".to_string())); 403 assert!(s.ts == Some(UNIX_EPOCH + Duration::new(1353832234, 0))); 404 assert!(s.nonce == Some("j4h3g2".to_string())); 405 assert!( 406 s.mac 407 == Some(Mac::from(vec![ 408 233, 30, 43, 87, 152, 132, 248, 211, 232, 202, 111, 150, 194, 55, 135, 206, 48, 409 6, 93, 75, 75, 52, 140, 102, 163, 91, 233, 50, 135, 233, 44, 1 410 ])) 411 ); 412 assert!(s.ext == Some("some-app-ext-data".to_string())); 413 assert!(s.app == None); 414 assert!(s.dlg == None); 415 } 416 417 #[test] to_str_no_fields()418 fn to_str_no_fields() { 419 // must supply a type for S, since it is otherwise unused 420 let s = Header::new::<String>(None, None, None, None, None, None, None, None).unwrap(); 421 let formatted = format!("{}", s); 422 println!("got: {}", formatted); 423 assert!(formatted == "") 424 } 425 426 #[test] to_str_few_fields()427 fn to_str_few_fields() { 428 let s = Header::new( 429 Some("dh37fgj492je"), 430 Some(UNIX_EPOCH + Duration::new(1353832234, 0)), 431 Some("j4h3g2"), 432 Some(Mac::from(vec![ 433 8, 35, 182, 149, 42, 111, 33, 192, 19, 22, 94, 43, 118, 176, 65, 69, 86, 4, 156, 434 184, 85, 107, 249, 242, 172, 200, 66, 209, 57, 63, 38, 83, 435 ])), 436 None, 437 None, 438 None, 439 None, 440 ) 441 .unwrap(); 442 let formatted = format!("{}", s); 443 println!("got: {}", formatted); 444 assert!( 445 formatted 446 == "id=\"dh37fgj492je\", ts=\"1353832234\", nonce=\"j4h3g2\", \ 447 mac=\"CCO2lSpvIcATFl4rdrBBRVYEnLhVa/nyrMhC0Tk/JlM=\"" 448 ) 449 } 450 451 #[test] to_str_maximal()452 fn to_str_maximal() { 453 let s = Header::new( 454 Some("dh37fgj492je"), 455 Some(UNIX_EPOCH + Duration::new(1353832234, 0)), 456 Some("j4h3g2"), 457 Some(Mac::from(vec![ 458 8, 35, 182, 149, 42, 111, 33, 192, 19, 22, 94, 43, 118, 176, 65, 69, 86, 4, 156, 459 184, 85, 107, 249, 242, 172, 200, 66, 209, 57, 63, 38, 83, 460 ])), 461 Some("my-ext-value"), 462 Some(vec![1, 2, 3, 4]), 463 Some("my-app"), 464 Some("my-dlg"), 465 ) 466 .unwrap(); 467 let formatted = format!("{}", s); 468 println!("got: {}", formatted); 469 assert!( 470 formatted 471 == "id=\"dh37fgj492je\", ts=\"1353832234\", nonce=\"j4h3g2\", \ 472 mac=\"CCO2lSpvIcATFl4rdrBBRVYEnLhVa/nyrMhC0Tk/JlM=\", ext=\"my-ext-value\", \ 473 hash=\"AQIDBA==\", app=\"my-app\", dlg=\"my-dlg\"" 474 ) 475 } 476 477 #[test] round_trip()478 fn round_trip() { 479 let s = Header::new( 480 Some("dh37fgj492je"), 481 Some(UNIX_EPOCH + Duration::new(1353832234, 0)), 482 Some("j4h3g2"), 483 Some(Mac::from(vec![ 484 8, 35, 182, 149, 42, 111, 33, 192, 19, 22, 94, 43, 118, 176, 65, 69, 86, 4, 156, 485 184, 85, 107, 249, 242, 172, 200, 66, 209, 57, 63, 38, 83, 486 ])), 487 Some("my-ext-value"), 488 Some(vec![1, 2, 3, 4]), 489 Some("my-app"), 490 Some("my-dlg"), 491 ) 492 .unwrap(); 493 let formatted = format!("{}", s); 494 println!("got: {}", s); 495 let s2 = Header::from_str(&formatted).unwrap(); 496 assert!(s2 == s); 497 } 498 } 499