1use crate::{
4 MyError, V200,
5 data::{MyLanguageTag, MyVersion},
6 runtime_error,
7};
8use etag::EntityTag;
9use rocket::{
10 Request,
11 http::{ContentType, Status, hyper::header},
12 request::{FromRequest, Outcome},
13};
14use std::{borrow::Cow, cmp::Ordering, ops::RangeInclusive, str::FromStr};
15use tracing::{debug, error, warn};
16
17pub const CONTENT_TRANSFER_ENCODING_HDR: &str = "Content-Transfer-Encoding";
19
20pub const VERSION_HDR: &str = "X-Experience-API-Version";
22
23pub const HASH_HDR: &str = "X-Experience-API-Hash";
25
26pub const CONSISTENT_THRU_HDR: &str = "X-Experience-API-Consistent-Through";
28
29const Q_RANGE: RangeInclusive<f32> = RangeInclusive::new(0.0, 1.0);
31
32#[derive(Debug)]
33enum ETagValue {
34 Absent,
36 Any,
38 Set(Vec<EntityTag>),
40}
41
42#[derive(Debug)]
44pub(crate) struct Headers {
45 #[allow(dead_code)]
55 version: String,
56 if_match_etags: ETagValue,
58 if_none_match_etags: ETagValue,
60 #[allow(dead_code)]
63 languages: Vec<MyLanguageTag>,
64 is_json_content: bool,
69}
70
71#[derive(Debug)]
76pub(crate) struct Language {
77 tag: MyLanguageTag,
84 q: u32,
94}
95
96impl TryFrom<&str> for Language {
97 type Error = MyError;
98
99 fn try_from(value: &str) -> Result<Self, Self::Error> {
105 if value.is_empty() {
106 runtime_error!("Input string must not be empty")
107 }
108
109 let pair: Vec<&str> = value.split(';').collect();
110 match MyLanguageTag::from_str(pair[0]) {
112 Ok(tag) => {
113 let mut q = 0.0;
116 if pair.len() > 1 {
117 let qv: Vec<&str> = pair[1].split('=').collect();
118 if qv[0] != "q" {
119 warn!("Q part in '{}' is malformed", pair[0]);
120 } else {
121 match qv[1].parse::<f32>() {
122 Ok(x) => {
123 if !Q_RANGE.contains(&x) {
124 warn!("Q in '{}' is out-of-bounds", pair[0]);
125 } else {
126 q = x;
127 }
128 }
129 Err(x) => warn!("Failed parsing Q w/in '{}': {}", pair[0], x),
130 }
131 }
132 } else {
133 q = 1.0;
134 }
135 Ok(Language {
136 tag,
137 q: (q * 1_000.0).round() as u32,
138 })
139 }
140 Err(x) => runtime_error!("Failed parsing Tag in '{}': {}", pair[0], x),
141 }
142 }
143}
144
145impl Default for Headers {
146 fn default() -> Self {
147 Self {
148 version: V200.to_owned(),
149 if_match_etags: ETagValue::Absent,
150 if_none_match_etags: ETagValue::Absent,
151 languages: vec![],
152 is_json_content: false,
153 }
154 }
155}
156
157#[rocket::async_trait]
158impl<'r> FromRequest<'r> for Headers {
159 type Error = MyError;
160
161 async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
162 let version = match req.headers().get_one(VERSION_HDR) {
163 Some(x) => match MyVersion::from_str(x) {
164 Ok(x) => {
165 if x.to_string() != V200 {
166 let msg = format!("xAPI v.{x} wanted but i only support 2.0.0");
167 error!("{}", msg);
168 return Outcome::Error((Status::BadRequest, MyError::Runtime(msg.into())));
170 }
171 x
172 }
173 Err(y) => {
174 let msg = format!("xAPI version header ({x}) has invalid syntax: {y}");
175 error!("{}", msg);
176 return Outcome::Error((Status::BadRequest, MyError::Runtime(msg.into())));
177 }
178 },
179 None => {
180 let msg = "Missing xAPI version header";
181 error!("{}", msg);
182 return Outcome::Error((Status::BadRequest, MyError::Runtime(Cow::Borrowed(msg))));
183 }
184 };
185
186 let if_match_etags = if req.headers().contains(header::IF_MATCH) {
187 let mut any = false;
188 let mut v1 = vec![];
189 for h in req.headers().get(header::IF_MATCH.as_str()) {
190 let h = h.trim();
191 debug!("h = '{}'", h);
192 if h == "*" {
193 any = true;
194 break;
195 } else {
196 let parts = h.split(',');
197 for p in parts {
198 match EntityTag::from_str(p.trim()) {
199 Ok(x) => v1.push(x),
200 Err(x) => error!(
201 "Malformed If-Match ({}) entity tag. Ignore + continue: {}",
202 p, x
203 ),
204 }
205 }
206 }
207 }
208 if any {
209 ETagValue::Any
210 } else if v1.is_empty() {
211 ETagValue::Absent
212 } else {
213 ETagValue::Set(v1)
214 }
215 } else {
216 ETagValue::Absent
217 };
218
219 let if_none_match_etags = if req.headers().contains(header::IF_NONE_MATCH) {
220 let mut any = false;
221 let mut v2 = vec![];
222 for h in req.headers().get(header::IF_NONE_MATCH.as_str()) {
223 let h = h.trim();
224 debug!("h = '{}'", h);
225 if h == "*" {
226 any = true;
227 break;
228 } else {
229 let parts = h.split(',');
230 for p in parts {
231 match EntityTag::from_str(p.trim()) {
232 Ok(x) => v2.push(x),
233 Err(x) => error!(
234 "Malformed If-None-Match ({}) entity tag. Ignore + continue: {}",
235 p, x
236 ),
237 }
238 }
239 }
240 }
241 if any {
242 ETagValue::Any
243 } else if v2.is_empty() {
244 ETagValue::Absent
245 } else {
246 ETagValue::Set(v2)
247 }
248 } else {
249 ETagValue::Absent
250 };
251
252 let languages = match req.headers().get_one(header::ACCEPT_LANGUAGE.as_str()) {
253 Some(x) => process_accept_language(x),
254 None => vec![],
255 };
256
257 let is_json_content = req.content_type().is_some_and(|h| *h == ContentType::JSON);
258
259 Outcome::Success(Headers {
260 version: version.to_string(),
261 if_match_etags,
262 if_none_match_etags,
263 languages,
264 is_json_content,
265 })
266 }
267}
268
269fn process_accept_language(s: &str) -> Vec<MyLanguageTag> {
270 let mut tuples = vec![];
271 let binding = s.replace(' ', "");
274 let tokens: Vec<&str> = binding.split(',').collect();
275 for t in tokens {
276 if let Ok(x) = Language::try_from(t) {
277 tuples.push(x)
278 }
279 }
280 if tuples.is_empty() {
281 return vec![];
282 }
283
284 tuples.sort_by(|x, y| match x.q.cmp(&y.q) {
286 Ordering::Less => Ordering::Greater,
287 Ordering::Greater => Ordering::Less,
288 Ordering::Equal => x.tag.as_str().cmp(y.tag.as_str()),
289 });
290
291 tuples.iter().map(|x| x.tag.to_owned()).collect()
292}
293
294impl Headers {
295 pub(crate) fn has_no_conditionals(&self) -> bool {
296 matches!(self.if_match_etags, ETagValue::Absent)
297 && matches!(self.if_none_match_etags, ETagValue::Absent)
298 }
299
300 pub(crate) fn has_conditionals(&self) -> bool {
301 self.has_if_match() || self.has_if_none_match()
302 }
303
304 pub(crate) fn has_if_match(&self) -> bool {
305 !matches!(self.if_match_etags, ETagValue::Absent)
306 }
307
308 pub(crate) fn pass_if_match(&self, etag: &EntityTag) -> bool {
309 if self.is_match_any() {
310 true
311 } else {
312 self.match_values()
313 .unwrap()
314 .iter()
315 .any(|x| x.strong_eq(etag))
316 }
317 }
318
319 pub(crate) fn pass_if_none_match(&self, etag: &EntityTag) -> bool {
320 if self.is_none_match_any() {
321 true
322 } else {
323 self.none_match_values()
324 .unwrap()
325 .iter()
326 .all(|x| x.weak_ne(etag))
327 }
328 }
329
330 pub(crate) fn languages(&self) -> &[MyLanguageTag] {
331 self.languages.as_slice()
332 }
333
334 pub(crate) fn is_json_content(&self) -> bool {
335 self.is_json_content
336 }
337
338 fn is_match_any(&self) -> bool {
339 matches!(self.if_match_etags, ETagValue::Any)
340 }
341
342 fn match_values(&self) -> Option<&Vec<EntityTag>> {
343 match &self.if_match_etags {
344 ETagValue::Set(x) => Some(x),
345 _ => None,
346 }
347 }
348
349 fn has_if_none_match(&self) -> bool {
350 !matches!(self.if_none_match_etags, ETagValue::Absent)
351 }
352
353 fn is_none_match_any(&self) -> bool {
354 matches!(self.if_none_match_etags, ETagValue::Any)
355 }
356
357 fn none_match_values(&self) -> Option<&Vec<EntityTag>> {
358 match &self.if_none_match_etags {
359 ETagValue::Set(x) => Some(x),
360 _ => None,
361 }
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use tracing_test::traced_test;
369
370 #[traced_test]
371 #[test]
372 fn test_sort_order_parsing_al() {
373 const TV: &str = "en-AU; q = 0.8, , en;q=0.1 , en-GB, en-US;q=0.9,";
374
375 let tags = process_accept_language(TV);
376 assert!(!tags.is_empty());
377 assert_eq!(tags.len(), 4);
378 let cv = vec![
379 "en-GB".to_string(),
380 "en-US".to_string(),
381 "en-AU".to_string(),
382 "en".to_string(),
383 ];
384 for i in 0..4 {
385 assert_eq!(tags[i], cv[i])
386 }
387 }
388
389 #[traced_test]
390 #[test]
391 fn test_leniency_parsing_al() {
392 const TV: &str = "fr-CA;q=0.8,foo,fr-LB;p=0.99,fr-FR,fr;q=0.25";
393
394 let tags = process_accept_language(TV);
395 assert!(!tags.is_empty());
396 assert_eq!(tags.len(), 4);
397 let cv = vec![
398 "fr-FR".to_string(),
399 "fr-CA".to_string(),
400 "fr".to_string(),
401 "fr-LB".to_string(),
402 ];
403 for i in 0..4 {
404 assert_eq!(tags[i], cv[i])
405 }
406 }
407}