1use crate::{Error, HttpRequest, HttpResponse};
55use bytes::Bytes;
56use std::fmt;
57use std::hash::{Hash, Hasher};
58use std::time::SystemTime;
59
60#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct ETag {
85 pub value: String,
87 pub weak: bool,
89}
90
91impl ETag {
92 pub fn strong(value: impl Into<String>) -> Self {
97 Self {
98 value: value.into(),
99 weak: false,
100 }
101 }
102
103 pub fn weak(value: impl Into<String>) -> Self {
108 Self {
109 value: value.into(),
110 weak: true,
111 }
112 }
113
114 pub fn parse(s: &str) -> Option<Self> {
130 let s = s.trim();
131
132 let (weak, value_part) = if s.starts_with("W/") || s.starts_with("w/") {
133 (true, &s[2..])
134 } else {
135 (false, s)
136 };
137
138 let value = value_part.strip_prefix('"')?.strip_suffix('"')?.to_string();
140
141 Some(Self { value, weak })
142 }
143
144 pub fn from_bytes(data: &[u8]) -> Self {
156 use std::collections::hash_map::DefaultHasher;
157
158 let mut hasher = DefaultHasher::new();
159 data.hash(&mut hasher);
160 let hash = hasher.finish();
161
162 Self::strong(format!("{:x}", hash))
163 }
164
165 pub fn weak_from_bytes(data: &[u8]) -> Self {
167 let mut etag = Self::from_bytes(data);
168 etag.weak = true;
169 etag
170 }
171
172 #[allow(clippy::should_implement_trait)]
174 pub fn from_str(s: &str) -> Self {
175 Self::from_bytes(s.as_bytes())
176 }
177
178 pub fn from_file_metadata(size: u64, modified: SystemTime) -> Self {
182 let modified_unix = modified
183 .duration_since(SystemTime::UNIX_EPOCH)
184 .map(|d| d.as_secs())
185 .unwrap_or(0);
186
187 Self::strong(format!("{:x}-{:x}", size, modified_unix))
188 }
189
190 pub fn from_version(version: u64) -> Self {
192 Self::strong(format!("v{}", version))
193 }
194
195 pub fn to_header_value(&self) -> String {
197 if self.weak {
198 format!("W/\"{}\"", self.value)
199 } else {
200 format!("\"{}\"", self.value)
201 }
202 }
203
204 pub fn strong_match(&self, other: &ETag) -> bool {
208 !self.weak && !other.weak && self.value == other.value
209 }
210
211 pub fn weak_match(&self, other: &ETag) -> bool {
215 self.value == other.value
216 }
217}
218
219impl fmt::Display for ETag {
220 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221 write!(f, "{}", self.to_header_value())
222 }
223}
224
225#[derive(Debug, Clone, Default)]
231pub struct ETagList {
232 pub etags: Vec<ETag>,
234 pub any: bool,
236}
237
238impl ETagList {
239 pub fn new() -> Self {
241 Self::default()
242 }
243
244 pub fn any() -> Self {
246 Self {
247 etags: Vec::new(),
248 any: true,
249 }
250 }
251
252 pub fn parse(header: &str) -> Self {
266 let header = header.trim();
267
268 if header == "*" {
270 return Self::any();
271 }
272
273 let etags: Vec<ETag> = header
274 .split(',')
275 .filter_map(|s| ETag::parse(s.trim()))
276 .collect();
277
278 Self { etags, any: false }
279 }
280
281 pub fn contains_weak(&self, etag: &ETag) -> bool {
283 if self.any {
284 return true;
285 }
286 self.etags.iter().any(|e| e.weak_match(etag))
287 }
288
289 pub fn contains_strong(&self, etag: &ETag) -> bool {
291 if self.any {
292 return true;
295 }
296 self.etags.iter().any(|e| e.strong_match(etag))
297 }
298
299 pub fn is_empty(&self) -> bool {
301 !self.any && self.etags.is_empty()
302 }
303}
304
305#[derive(Debug, Clone, Default)]
311pub struct ConditionalHeaders {
312 pub if_none_match: Option<ETagList>,
314 pub if_match: Option<ETagList>,
316 pub if_modified_since: Option<SystemTime>,
318 pub if_unmodified_since: Option<SystemTime>,
320}
321
322impl ConditionalHeaders {
323 pub fn from_request(request: &HttpRequest) -> Self {
329 let if_none_match = request.headers.get("If-None-Match").map(ETagList::parse);
332
333 let if_match = request.headers.get("If-Match").map(ETagList::parse);
334
335 let if_modified_since = request
336 .headers
337 .get("If-Modified-Since")
338 .and_then(|h| httpdate::parse_http_date(h).ok());
339
340 let if_unmodified_since = request
341 .headers
342 .get("If-Unmodified-Since")
343 .and_then(|h| httpdate::parse_http_date(h).ok());
344
345 Self {
346 if_none_match,
347 if_match,
348 if_modified_since,
349 if_unmodified_since,
350 }
351 }
352
353 pub fn is_not_modified(&self, etag: Option<&ETag>, last_modified: Option<SystemTime>) -> bool {
359 if let Some(ref if_none_match) = self.if_none_match
361 && let Some(etag) = etag
362 {
363 return if_none_match.contains_weak(etag);
364 }
365
366 if let (Some(if_modified_since), Some(last_modified)) =
368 (self.if_modified_since, last_modified)
369 {
370 return last_modified <= if_modified_since;
371 }
372
373 false
374 }
375
376 pub fn precondition_failed(
382 &self,
383 etag: Option<&ETag>,
384 last_modified: Option<SystemTime>,
385 ) -> bool {
386 if let Some(ref if_match) = self.if_match {
388 if let Some(etag) = etag {
389 return !if_match.contains_strong(etag);
390 } else {
391 return !if_match.any;
393 }
394 }
395
396 if let (Some(if_unmodified_since), Some(last_modified)) =
398 (self.if_unmodified_since, last_modified)
399 {
400 return last_modified > if_unmodified_since;
401 }
402
403 false
404 }
405}
406
407pub trait ConditionalRequest {
413 fn conditional_headers(&self) -> ConditionalHeaders;
415
416 fn if_none_match(&self) -> Option<ETagList>;
418
419 fn if_match(&self) -> Option<ETagList>;
421
422 fn if_modified_since(&self) -> Option<SystemTime>;
424
425 fn if_unmodified_since(&self) -> Option<SystemTime>;
427
428 fn if_none_match_matches(&self, etag: &ETag) -> bool;
432
433 fn if_match_matches(&self, etag: &ETag) -> bool;
437
438 fn not_modified_since(&self, last_modified: SystemTime) -> bool;
440
441 fn modified_since_precondition(&self, last_modified: SystemTime) -> bool;
443
444 fn evaluate_conditionals(
451 &self,
452 etag: Option<&ETag>,
453 last_modified: Option<SystemTime>,
454 ) -> Option<u16>;
455}
456
457impl ConditionalRequest for HttpRequest {
458 fn conditional_headers(&self) -> ConditionalHeaders {
459 ConditionalHeaders::from_request(self)
460 }
461
462 fn if_none_match(&self) -> Option<ETagList> {
465 self.headers.get("If-None-Match").map(ETagList::parse)
466 }
467
468 fn if_match(&self) -> Option<ETagList> {
469 self.headers.get("If-Match").map(ETagList::parse)
470 }
471
472 fn if_modified_since(&self) -> Option<SystemTime> {
473 self.headers
474 .get("If-Modified-Since")
475 .and_then(|h| httpdate::parse_http_date(h).ok())
476 }
477
478 fn if_unmodified_since(&self) -> Option<SystemTime> {
479 self.headers
480 .get("If-Unmodified-Since")
481 .and_then(|h| httpdate::parse_http_date(h).ok())
482 }
483
484 fn if_none_match_matches(&self, etag: &ETag) -> bool {
485 self.if_none_match()
486 .map(|list| list.contains_weak(etag))
487 .unwrap_or(false)
488 }
489
490 fn if_match_matches(&self, etag: &ETag) -> bool {
491 match self.if_match() {
492 Some(list) => list.contains_strong(etag),
493 None => true, }
495 }
496
497 fn not_modified_since(&self, last_modified: SystemTime) -> bool {
498 self.if_modified_since()
499 .map(|since| last_modified <= since)
500 .unwrap_or(false)
501 }
502
503 fn modified_since_precondition(&self, last_modified: SystemTime) -> bool {
504 self.if_unmodified_since()
505 .map(|since| last_modified > since)
506 .unwrap_or(false)
507 }
508
509 fn evaluate_conditionals(
510 &self,
511 etag: Option<&ETag>,
512 last_modified: Option<SystemTime>,
513 ) -> Option<u16> {
514 let headers = self.conditional_headers();
515
516 if headers.precondition_failed(etag, last_modified) {
518 return Some(412);
519 }
520
521 let is_safe = matches!(self.method, crate::Method::Get | crate::Method::Head);
524
525 if is_safe {
526 if headers.is_not_modified(etag, last_modified) {
528 return Some(304);
529 }
530 } else if let (Some(if_none_match), Some(etag)) = (&headers.if_none_match, etag) {
531 if if_none_match.contains_weak(etag) {
534 return Some(412);
535 }
536 }
537
538 None
539 }
540}
541
542pub trait ConditionalResponse {
548 fn with_etag(self, etag: &ETag) -> Self;
550
551 fn with_last_modified(self, time: SystemTime) -> Self;
553
554 fn not_modified() -> Self;
556
557 fn not_modified_with_etag(etag: &ETag) -> Self;
559
560 fn precondition_failed() -> Self;
562
563 fn precondition_failed_with_message(message: &str) -> Self;
565}
566
567impl ConditionalResponse for HttpResponse {
568 fn with_etag(mut self, etag: &ETag) -> Self {
569 self.headers
570 .insert("ETag".to_string(), etag.to_header_value());
571 self
572 }
573
574 fn with_last_modified(mut self, time: SystemTime) -> Self {
575 let formatted = httpdate::fmt_http_date(time);
576 self.headers.insert("Last-Modified".to_string(), formatted);
577 self
578 }
579
580 fn not_modified() -> Self {
581 Self::new(304)
582 }
583
584 fn not_modified_with_etag(etag: &ETag) -> Self {
585 let mut response = Self::new(304);
586 response
587 .headers
588 .insert("ETag".to_string(), etag.to_header_value());
589 response
590 }
591
592 fn precondition_failed() -> Self {
593 Self::new(412)
594 }
595
596 fn precondition_failed_with_message(message: &str) -> Self {
597 let body = serde_json::json!({
598 "error": "Precondition Failed",
599 "message": message,
600 "status": 412
601 });
602
603 let mut response = Self::new(412);
604 if let Ok(body_bytes) = serde_json::to_vec(&body) {
605 response.body = Bytes::from(body_bytes);
606 response
607 .headers
608 .insert("Content-Type".to_string(), "application/json".to_string());
609 }
610 response
611 }
612}
613
614pub fn check_conditionals(
649 request: &HttpRequest,
650 etag: Option<&ETag>,
651 last_modified: Option<SystemTime>,
652) -> Option<HttpResponse> {
653 match request.evaluate_conditionals(etag, last_modified) {
654 Some(304) => {
655 let mut response = HttpResponse::not_modified();
656 if let Some(etag) = etag {
657 response = response.with_etag(etag);
658 }
659 if let Some(lm) = last_modified {
660 response = response.with_last_modified(lm);
661 }
662 Some(response)
663 }
664 Some(412) => Some(HttpResponse::precondition_failed_with_message(
665 "Resource has been modified",
666 )),
667 _ => None,
668 }
669}
670
671pub fn cacheable_response<T: serde::Serialize>(
683 data: &T,
684 etag: &ETag,
685 last_modified: Option<SystemTime>,
686) -> Result<HttpResponse, Error> {
687 let mut response = HttpResponse::ok().with_json(data)?.with_etag(etag);
688
689 if let Some(lm) = last_modified {
690 response = response.with_last_modified(lm);
691 }
692
693 response.headers.insert(
695 "Cache-Control".to_string(),
696 "private, must-revalidate".to_string(),
697 );
698 response
699 .headers
700 .insert("Vary".to_string(), "Accept, Accept-Encoding".to_string());
701
702 Ok(response)
703}
704
705#[cfg(test)]
710mod tests {
711 use super::*;
712
713 #[test]
714 fn test_etag_strong() {
715 let etag = ETag::strong("abc123");
716 assert!(!etag.weak);
717 assert_eq!(etag.value, "abc123");
718 assert_eq!(etag.to_header_value(), "\"abc123\"");
719 }
720
721 #[test]
722 fn test_etag_weak() {
723 let etag = ETag::weak("abc123");
724 assert!(etag.weak);
725 assert_eq!(etag.value, "abc123");
726 assert_eq!(etag.to_header_value(), "W/\"abc123\"");
727 }
728
729 #[test]
730 fn test_etag_parse_strong() {
731 let etag = ETag::parse("\"abc123\"").unwrap();
732 assert!(!etag.weak);
733 assert_eq!(etag.value, "abc123");
734 }
735
736 #[test]
737 fn test_etag_parse_weak() {
738 let etag = ETag::parse("W/\"abc123\"").unwrap();
739 assert!(etag.weak);
740 assert_eq!(etag.value, "abc123");
741 }
742
743 #[test]
744 fn test_etag_parse_weak_lowercase() {
745 let etag = ETag::parse("w/\"abc123\"").unwrap();
746 assert!(etag.weak);
747 assert_eq!(etag.value, "abc123");
748 }
749
750 #[test]
751 fn test_etag_from_bytes() {
752 let data = b"Hello, World!";
753 let etag1 = ETag::from_bytes(data);
754 let etag2 = ETag::from_bytes(data);
755 assert_eq!(etag1.value, etag2.value);
756 assert!(!etag1.weak);
757 }
758
759 #[test]
760 fn test_etag_from_version() {
761 let etag = ETag::from_version(42);
762 assert_eq!(etag.value, "v42");
763 assert!(!etag.weak);
764 }
765
766 #[test]
767 fn test_etag_strong_match() {
768 let e1 = ETag::strong("abc");
769 let e2 = ETag::strong("abc");
770 let e3 = ETag::weak("abc");
771
772 assert!(e1.strong_match(&e2));
773 assert!(!e1.strong_match(&e3)); }
775
776 #[test]
777 fn test_etag_weak_match() {
778 let e1 = ETag::strong("abc");
779 let e2 = ETag::weak("abc");
780
781 assert!(e1.weak_match(&e2)); }
783
784 #[test]
785 fn test_etag_list_parse() {
786 let list = ETagList::parse("\"abc\", \"def\", W/\"ghi\"");
787 assert_eq!(list.etags.len(), 3);
788 assert!(!list.any);
789 }
790
791 #[test]
792 fn test_etag_list_parse_wildcard() {
793 let list = ETagList::parse("*");
794 assert!(list.any);
795 assert!(list.etags.is_empty());
796 }
797
798 #[test]
799 fn test_etag_list_contains_weak() {
800 let list = ETagList::parse("\"abc\", W/\"def\"");
801 let strong_abc = ETag::strong("abc");
802 let weak_abc = ETag::weak("abc");
803 let strong_xyz = ETag::strong("xyz");
804
805 assert!(list.contains_weak(&strong_abc));
806 assert!(list.contains_weak(&weak_abc)); assert!(!list.contains_weak(&strong_xyz));
808 }
809
810 #[test]
811 fn test_etag_list_contains_strong() {
812 let list = ETagList::parse("\"abc\", W/\"def\"");
813 let strong_abc = ETag::strong("abc");
814 let weak_abc = ETag::weak("abc");
815 let strong_def = ETag::strong("def");
816
817 assert!(list.contains_strong(&strong_abc));
818 assert!(!list.contains_strong(&weak_abc)); assert!(!list.contains_strong(&strong_def)); }
821
822 #[test]
823 fn test_etag_list_wildcard_contains() {
824 let list = ETagList::any();
825 let etag = ETag::strong("anything");
826
827 assert!(list.contains_weak(&etag));
828 assert!(list.contains_strong(&etag));
829 }
830
831 #[test]
832 fn test_etag_list_wildcard_matches_weak_etag() {
833 let list = ETagList::any();
836 let weak = ETag::weak("abc123");
837
838 assert!(list.contains_strong(&weak));
839 assert!(list.contains_weak(&weak));
840 }
841
842 #[test]
843 fn test_if_match_wildcard_with_weak_etag_succeeds() {
844 let mut request = HttpRequest::new("PUT", "/resource".to_string());
845 request.headers.insert("If-Match", "*".to_string());
846
847 let weak = ETag::weak("abc123");
848 assert_eq!(request.evaluate_conditionals(Some(&weak), None), None);
849 }
850
851 #[test]
852 fn test_if_none_match_unsafe_method_412() {
853 for method in ["PUT", "POST", "DELETE", "PATCH"] {
855 let mut request = HttpRequest::new(method.to_string(), "/resource".to_string());
856 request
857 .headers
858 .insert("If-None-Match", "\"abc123\"".to_string());
859
860 let etag = ETag::strong("abc123");
861 assert_eq!(
862 request.evaluate_conditionals(Some(&etag), None),
863 Some(412),
864 "expected 412 for {}",
865 method
866 );
867 }
868 }
869
870 #[test]
871 fn test_if_none_match_unsafe_method_no_match_proceeds() {
872 let mut request = HttpRequest::new("PUT", "/resource".to_string());
873 request
874 .headers
875 .insert("If-None-Match", "\"abc123\"".to_string());
876
877 let etag = ETag::strong("different");
878 assert_eq!(request.evaluate_conditionals(Some(&etag), None), None);
879 }
880
881 #[test]
882 fn test_conditional_headers_if_none_match() {
883 let mut request = HttpRequest::new("GET", "/resource".to_string());
884 request
885 .headers
886 .insert("If-None-Match", "\"abc123\"".to_string());
887
888 let headers = ConditionalHeaders::from_request(&request);
889 assert!(headers.if_none_match.is_some());
890
891 let etag = ETag::strong("abc123");
892 assert!(headers.is_not_modified(Some(&etag), None));
893 }
894
895 #[test]
899 fn test_conditional_headers_lowercase_header_names() {
900 let mut request = HttpRequest::new("GET".to_string(), "/resource".to_string());
901 request
902 .headers
903 .insert("if-none-match", "\"abc123\"".to_string());
904 request.headers.insert("if-match", "\"abc123\"".to_string());
905 request.headers.insert(
906 "if-modified-since",
907 "Sun, 06 Nov 1994 08:49:37 GMT".to_string(),
908 );
909 request.headers.insert(
910 "if-unmodified-since",
911 "Sun, 06 Nov 1994 08:49:37 GMT".to_string(),
912 );
913
914 let headers = ConditionalHeaders::from_request(&request);
915 assert!(headers.if_none_match.is_some());
916 assert!(headers.if_match.is_some());
917 assert!(headers.if_modified_since.is_some());
918 assert!(headers.if_unmodified_since.is_some());
919
920 let etag = ETag::strong("abc123");
921 assert!(headers.is_not_modified(Some(&etag), None));
922
923 assert!(request.if_none_match().is_some());
925 assert!(request.if_match().is_some());
926 assert!(request.if_modified_since().is_some());
927 assert!(request.if_unmodified_since().is_some());
928 }
929
930 #[test]
931 fn test_conditional_headers_if_match() {
932 let mut request = HttpRequest::new("PUT", "/resource".to_string());
933 request.headers.insert("If-Match", "\"abc123\"".to_string());
934
935 let headers = ConditionalHeaders::from_request(&request);
936 assert!(headers.if_match.is_some());
937
938 let matching = ETag::strong("abc123");
939 let non_matching = ETag::strong("xyz789");
940
941 assert!(!headers.precondition_failed(Some(&matching), None));
942 assert!(headers.precondition_failed(Some(&non_matching), None));
943 }
944
945 #[test]
946 fn test_request_if_none_match_matches() {
947 let mut request = HttpRequest::new("GET", "/resource".to_string());
948 request
949 .headers
950 .insert("If-None-Match", "\"abc123\"".to_string());
951
952 let matching = ETag::strong("abc123");
953 let non_matching = ETag::strong("xyz789");
954
955 assert!(request.if_none_match_matches(&matching));
956 assert!(!request.if_none_match_matches(&non_matching));
957 }
958
959 #[test]
960 fn test_request_if_match_matches() {
961 let mut request = HttpRequest::new("PUT", "/resource".to_string());
962 request.headers.insert("If-Match", "\"abc123\"".to_string());
963
964 let matching = ETag::strong("abc123");
965 let non_matching = ETag::strong("xyz789");
966
967 assert!(request.if_match_matches(&matching));
968 assert!(!request.if_match_matches(&non_matching));
969 }
970
971 #[test]
972 fn test_request_evaluate_conditionals_304() {
973 let mut request = HttpRequest::new("GET", "/resource".to_string());
974 request
975 .headers
976 .insert("If-None-Match", "\"abc123\"".to_string());
977
978 let etag = ETag::strong("abc123");
979 assert_eq!(request.evaluate_conditionals(Some(&etag), None), Some(304));
980 }
981
982 #[test]
983 fn test_request_evaluate_conditionals_412() {
984 let mut request = HttpRequest::new("PUT", "/resource".to_string());
985 request.headers.insert("If-Match", "\"abc123\"".to_string());
986
987 let etag = ETag::strong("xyz789");
988 assert_eq!(request.evaluate_conditionals(Some(&etag), None), Some(412));
989 }
990
991 #[test]
992 fn test_request_evaluate_conditionals_proceed() {
993 let request = HttpRequest::new("GET", "/resource".to_string());
994 let etag = ETag::strong("abc123");
995
996 assert_eq!(request.evaluate_conditionals(Some(&etag), None), None);
998 }
999
1000 #[test]
1001 fn test_response_with_etag() {
1002 let etag = ETag::strong("abc123");
1003 let response = HttpResponse::ok().with_etag(&etag);
1004
1005 assert_eq!(
1006 response.headers.get("ETag"),
1007 Some(&"\"abc123\"".to_string())
1008 );
1009 }
1010
1011 #[test]
1012 fn test_response_not_modified() {
1013 let response = HttpResponse::not_modified();
1014 assert_eq!(response.status, 304);
1015 }
1016
1017 #[test]
1018 fn test_response_precondition_failed() {
1019 let response = HttpResponse::precondition_failed();
1020 assert_eq!(response.status, 412);
1021 }
1022
1023 #[test]
1024 fn test_check_conditionals_returns_304() {
1025 let mut request = HttpRequest::new("GET", "/resource".to_string());
1026 request
1027 .headers
1028 .insert("If-None-Match", "\"abc123\"".to_string());
1029
1030 let etag = ETag::strong("abc123");
1031 let response = check_conditionals(&request, Some(&etag), None);
1032
1033 assert!(response.is_some());
1034 assert_eq!(response.unwrap().status, 304);
1035 }
1036
1037 #[test]
1038 fn test_check_conditionals_returns_412() {
1039 let mut request = HttpRequest::new("PUT", "/resource".to_string());
1040 request.headers.insert("If-Match", "\"abc123\"".to_string());
1041
1042 let etag = ETag::strong("different");
1043 let response = check_conditionals(&request, Some(&etag), None);
1044
1045 assert!(response.is_some());
1046 assert_eq!(response.unwrap().status, 412);
1047 }
1048
1049 #[test]
1050 fn test_check_conditionals_returns_none() {
1051 let request = HttpRequest::new("GET", "/resource".to_string());
1052 let etag = ETag::strong("abc123");
1053
1054 let response = check_conditionals(&request, Some(&etag), None);
1055 assert!(response.is_none());
1056 }
1057}