1use http::request::Parts;
2
3pub trait TokenSource: Send + Sync {
8 fn extract(&self, parts: &Parts) -> Option<String>;
11}
12
13pub struct BearerSource;
19
20impl TokenSource for BearerSource {
21 fn extract(&self, parts: &Parts) -> Option<String> {
22 let value = parts
23 .headers
24 .get(http::header::AUTHORIZATION)?
25 .to_str()
26 .ok()?;
27 let token = value
28 .strip_prefix("Bearer ")
29 .or_else(|| value.strip_prefix("bearer "))?;
30 if token.is_empty() {
31 return None;
32 }
33 Some(token.to_string())
34 }
35}
36
37pub struct QuerySource(pub &'static str);
41
42impl TokenSource for QuerySource {
43 fn extract(&self, parts: &Parts) -> Option<String> {
44 let query = parts.uri.query()?;
45 for pair in query.split('&') {
46 if let Some((key, value)) = pair.split_once('=')
47 && key == self.0
48 && !value.is_empty()
49 {
50 return Some(value.to_string());
51 }
52 }
53 None
54 }
55}
56
57pub struct CookieSource(pub &'static str);
62
63impl TokenSource for CookieSource {
64 fn extract(&self, parts: &Parts) -> Option<String> {
65 let cookie_header = parts.headers.get(http::header::COOKIE)?.to_str().ok()?;
66 for cookie in cookie_header.split(';') {
67 let cookie = cookie.trim();
68 if let Some((name, value)) = cookie.split_once('=')
69 && name.trim() == self.0
70 && !value.is_empty()
71 {
72 return Some(value.trim().to_string());
73 }
74 }
75 None
76 }
77}
78
79pub struct HeaderSource(pub &'static str);
83
84impl TokenSource for HeaderSource {
85 fn extract(&self, parts: &Parts) -> Option<String> {
86 let value = parts.headers.get(self.0)?.to_str().ok()?;
87 if value.is_empty() {
88 return None;
89 }
90 Some(value.to_string())
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 fn parts_with_header(name: &str, value: &str) -> Parts {
99 let (parts, _) = http::Request::builder()
100 .header(name, value)
101 .body(())
102 .unwrap()
103 .into_parts();
104 parts
105 }
106
107 fn parts_with_uri(uri: &str) -> Parts {
108 let (parts, _) = http::Request::builder()
109 .uri(uri)
110 .body(())
111 .unwrap()
112 .into_parts();
113 parts
114 }
115
116 fn empty_parts() -> Parts {
117 let (parts, _) = http::Request::builder().body(()).unwrap().into_parts();
118 parts
119 }
120
121 #[test]
123 fn bearer_extracts_token() {
124 let parts = parts_with_header("Authorization", "Bearer my-token");
125 assert_eq!(BearerSource.extract(&parts), Some("my-token".into()));
126 }
127
128 #[test]
129 fn bearer_case_insensitive_prefix() {
130 let parts = parts_with_header("Authorization", "bearer my-token");
131 assert_eq!(BearerSource.extract(&parts), Some("my-token".into()));
132 }
133
134 #[test]
135 fn bearer_returns_none_when_missing() {
136 assert!(BearerSource.extract(&empty_parts()).is_none());
137 }
138
139 #[test]
140 fn bearer_returns_none_for_non_bearer_scheme() {
141 let parts = parts_with_header("Authorization", "Basic abc123");
142 assert!(BearerSource.extract(&parts).is_none());
143 }
144
145 #[test]
146 fn bearer_returns_none_for_empty_token() {
147 let parts = parts_with_header("Authorization", "Bearer ");
148 assert!(BearerSource.extract(&parts).is_none());
149 }
150
151 #[test]
153 fn query_extracts_token() {
154 let parts = parts_with_uri("/path?token=my-token&other=val");
155 assert_eq!(
156 QuerySource("token").extract(&parts),
157 Some("my-token".into())
158 );
159 }
160
161 #[test]
162 fn query_returns_none_when_missing() {
163 let parts = parts_with_uri("/path?other=val");
164 assert!(QuerySource("token").extract(&parts).is_none());
165 }
166
167 #[test]
168 fn query_returns_none_for_empty_value() {
169 let parts = parts_with_uri("/path?token=");
170 assert!(QuerySource("token").extract(&parts).is_none());
171 }
172
173 #[test]
174 fn query_returns_none_without_query_string() {
175 let parts = parts_with_uri("/path");
176 assert!(QuerySource("token").extract(&parts).is_none());
177 }
178
179 #[test]
181 fn cookie_extracts_token() {
182 let parts = parts_with_header("Cookie", "jwt=my-token; other=val");
183 assert_eq!(CookieSource("jwt").extract(&parts), Some("my-token".into()));
184 }
185
186 #[test]
187 fn cookie_returns_none_when_missing() {
188 let parts = parts_with_header("Cookie", "other=val");
189 assert!(CookieSource("jwt").extract(&parts).is_none());
190 }
191
192 #[test]
193 fn cookie_returns_none_without_cookie_header() {
194 assert!(CookieSource("jwt").extract(&empty_parts()).is_none());
195 }
196
197 #[test]
199 fn header_extracts_token() {
200 let parts = parts_with_header("X-API-Token", "my-token");
201 assert_eq!(
202 HeaderSource("X-API-Token").extract(&parts),
203 Some("my-token".into())
204 );
205 }
206
207 #[test]
208 fn header_returns_none_when_missing() {
209 assert!(
210 HeaderSource("X-API-Token")
211 .extract(&empty_parts())
212 .is_none()
213 );
214 }
215
216 #[test]
217 fn header_returns_none_for_empty_value() {
218 let parts = parts_with_header("X-API-Token", "");
219 assert!(HeaderSource("X-API-Token").extract(&parts).is_none());
220 }
221}