1use crate::Result;
2use std::{
3 any::{Any, TypeId},
4 collections::HashMap,
5 sync::{Arc, RwLock},
6};
7
8pub struct RequestContext {
10 method: String,
11 path: String,
12 headers: HashMap<String, String>,
13 cookies: HashMap<String, String>,
14 extensions: RwLock<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
15}
16
17impl RequestContext {
18 pub fn new(
20 method: impl Into<String>,
21 path: impl Into<String>,
22 headers: HashMap<String, String>,
23 ) -> Self {
24 let headers = headers
25 .into_iter()
26 .map(|(name, value)| (name.to_ascii_lowercase(), value))
27 .collect::<HashMap<_, _>>();
28 let cookies = parse_cookies(headers.get("cookie").map(String::as_str));
29 Self {
30 method: method.into(),
31 path: path.into(),
32 headers,
33 cookies,
34 extensions: RwLock::new(HashMap::new()),
35 }
36 }
37
38 pub fn method(&self) -> &str {
40 &self.method
41 }
42
43 pub fn path(&self) -> &str {
45 &self.path
46 }
47
48 pub fn header(&self, name: &str) -> Option<&str> {
50 self.headers
51 .get(&name.to_ascii_lowercase())
52 .map(String::as_str)
53 }
54
55 pub fn bearer_token(&self) -> Option<&str> {
57 self.header("authorization")?.strip_prefix("Bearer ")
58 }
59
60 pub fn cookie(&self, name: &str) -> Option<&str> {
62 self.cookies.get(name).map(String::as_str)
63 }
64
65 pub fn set<T: Send + Sync + 'static>(&self, value: T) -> Result<()> {
67 self.extensions
68 .write()
69 .map_err(|_| {
70 crate::exception::startup_error("request context extensions lock poisoned")
71 })?
72 .insert(TypeId::of::<T>(), Arc::new(value));
73 Ok(())
74 }
75
76 pub fn get<T: Send + Sync + 'static>(&self) -> Result<Option<Arc<T>>> {
78 let value = self
79 .extensions
80 .read()
81 .map_err(|_| {
82 crate::exception::startup_error("request context extensions lock poisoned")
83 })?
84 .get(&TypeId::of::<T>())
85 .cloned();
86
87 let Some(value) = value else {
88 return Ok(None);
89 };
90
91 value
92 .clone()
93 .downcast::<T>()
94 .map(Some)
95 .map_err(|_| crate::exception::startup_error("request context extension type mismatch"))
96 }
97}
98
99fn parse_cookies(header: Option<&str>) -> HashMap<String, String> {
100 let mut cookies = HashMap::new();
101 for pair in header.into_iter().flat_map(|value| value.split(';')) {
102 let pair = pair.trim();
103 let Some((name, value)) = pair.split_once('=') else {
104 continue;
105 };
106 let name = name.trim();
107 if name.is_empty() {
108 continue;
109 }
110 let Ok(name) = cookie::Cookie::parse_encoded(format!("{name}=")) else {
111 continue;
112 };
113 let Ok(value) = cookie::Cookie::parse_encoded(format!("value={}", value.trim())) else {
114 continue;
115 };
116 cookies
117 .entry(name.name().to_string())
118 .or_insert_with(|| value.value().to_string());
119 }
120 cookies
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn parses_cookie_edge_cases_once() {
129 let ctx = RequestContext::new(
130 "GET",
131 "/",
132 HashMap::from([(
133 "Cookie".into(),
134 " theme = dark%20mode ; token=a=b=c; broken; =bad; dup=first; dup=second; na%6De=value"
135 .into(),
136 )]),
137 );
138 assert_eq!(ctx.cookie("theme"), Some("dark mode"));
139 assert_eq!(ctx.cookie("token"), Some("a=b=c"));
140 assert_eq!(ctx.cookie("dup"), Some("first"));
141 assert_eq!(ctx.cookie("name"), Some("value"));
142 assert_eq!(ctx.cookie("broken"), None);
143 }
144}