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