1use cookie::Cookie;
2use relay_protocol::{Annotated, Empty, Error, FromValue, IntoValue, Object, Value};
3use url::form_urlencoded;
4
5use crate::processor::ProcessValue;
6use crate::protocol::{JsonLenientString, LenientString, PairList};
7
8type CookieEntry = Annotated<(Annotated<String>, Annotated<String>)>;
9
10#[derive(Clone, Debug, Default, PartialEq, Empty, IntoValue, ProcessValue)]
12pub struct Cookies(pub PairList<(Annotated<String>, Annotated<String>)>);
13
14impl Cookies {
15 pub fn parse(string: &str) -> Result<Self, Error> {
16 let pairs: Result<_, _> = Self::iter_cookies(string).collect();
17 pairs.map(Cookies)
18 }
19
20 fn iter_cookies(string: &str) -> impl Iterator<Item = Result<CookieEntry, Error>> + '_ {
21 string
22 .split(';')
23 .filter(|cookie| !cookie.trim().is_empty())
24 .map(Cookies::parse_cookie)
25 }
26
27 fn parse_cookie(string: &str) -> Result<CookieEntry, Error> {
28 match Cookie::parse_encoded(string) {
29 Ok(cookie) => Ok(Annotated::from((
30 cookie.name().to_string().into(),
31 cookie.value().to_string().into(),
32 ))),
33 Err(error) => Err(Error::invalid(error)),
34 }
35 }
36}
37
38impl std::ops::Deref for Cookies {
39 type Target = PairList<(Annotated<String>, Annotated<String>)>;
40
41 fn deref(&self) -> &Self::Target {
42 &self.0
43 }
44}
45
46impl std::ops::DerefMut for Cookies {
47 fn deref_mut(&mut self) -> &mut Self::Target {
48 &mut self.0
49 }
50}
51
52impl FromValue for Cookies {
53 fn from_value(value: Annotated<Value>) -> Annotated<Self> {
54 match value {
55 Annotated(Some(Value::String(value)), mut meta) => {
56 let mut cookies = Vec::new();
57 for result in Cookies::iter_cookies(&value) {
58 match result {
59 Ok(cookie) => cookies.push(cookie),
60 Err(error) => meta.add_error(error),
61 }
62 }
63
64 if meta.has_errors() && meta.original_value().is_none() {
65 meta.set_original_value(Some(value));
66 }
67
68 Annotated(Some(Cookies(PairList(cookies))), meta)
69 }
70 annotated @ Annotated(Some(Value::Object(_)), _)
71 | annotated @ Annotated(Some(Value::Array(_)), _) => {
72 PairList::from_value(annotated).map_value(Cookies)
73 }
74 Annotated(None, meta) => Annotated(None, meta),
75 Annotated(Some(value), mut meta) => {
76 meta.add_error(Error::expected("cookies"));
77 meta.set_original_value(Some(value));
78 Annotated(None, meta)
79 }
80 }
81 }
82}
83
84#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Empty, IntoValue, ProcessValue)]
86#[metastructure(process_func = "process_header_name")]
87pub struct HeaderName(String);
88
89impl HeaderName {
90 pub fn new<S: AsRef<str>>(name: S) -> Self {
92 let name = name.as_ref();
93 let mut normalized = String::with_capacity(name.len());
94
95 name.chars().fold(true, |uppercase, c| {
96 if uppercase {
97 normalized.extend(c.to_uppercase());
98 } else {
99 normalized.push(c); }
101 c == '-'
102 });
103
104 HeaderName(normalized)
105 }
106
107 pub fn as_str(&self) -> &str {
109 &self.0
110 }
111
112 pub fn into_inner(self) -> String {
114 self.0
115 }
116}
117
118impl AsRef<str> for HeaderName {
119 fn as_ref(&self) -> &str {
120 &self.0
121 }
122}
123
124impl std::ops::Deref for HeaderName {
125 type Target = String;
126
127 fn deref(&self) -> &Self::Target {
128 &self.0
129 }
130}
131
132impl std::ops::DerefMut for HeaderName {
133 fn deref_mut(&mut self) -> &mut Self::Target {
134 &mut self.0
135 }
136}
137
138impl From<String> for HeaderName {
139 fn from(value: String) -> Self {
140 HeaderName::new(value)
141 }
142}
143
144impl From<&'_ str> for HeaderName {
145 fn from(value: &str) -> Self {
146 HeaderName::new(value)
147 }
148}
149
150impl FromValue for HeaderName {
151 fn from_value(value: Annotated<Value>) -> Annotated<Self> {
152 String::from_value(value).map_value(HeaderName::new)
153 }
154}
155
156#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Empty, IntoValue, ProcessValue)]
158pub struct HeaderValue(String);
159
160impl HeaderValue {
161 pub fn new<S: AsRef<str>>(value: S) -> Self {
162 HeaderValue(value.as_ref().to_owned())
163 }
164}
165
166impl AsRef<str> for HeaderValue {
167 fn as_ref(&self) -> &str {
168 &self.0
169 }
170}
171
172impl std::ops::Deref for HeaderValue {
173 type Target = String;
174
175 fn deref(&self) -> &Self::Target {
176 &self.0
177 }
178}
179
180impl std::ops::DerefMut for HeaderValue {
181 fn deref_mut(&mut self) -> &mut Self::Target {
182 &mut self.0
183 }
184}
185
186impl From<String> for HeaderValue {
187 fn from(value: String) -> Self {
188 HeaderValue::new(value)
189 }
190}
191
192impl From<&'_ str> for HeaderValue {
193 fn from(value: &str) -> Self {
194 HeaderValue::new(value)
195 }
196}
197
198impl FromValue for HeaderValue {
199 fn from_value(value: Annotated<Value>) -> Annotated<Self> {
200 match value {
201 Annotated(Some(Value::Array(array)), mut meta) => {
202 let mut header_value = String::new();
203 for array_value in array {
204 let array_value = LenientString::from_value(array_value);
205 for error in array_value.meta().iter_errors() {
206 meta.add_error(error.clone());
207 }
208
209 if let Some(string) = array_value.value() {
210 if !header_value.is_empty() {
211 header_value.push(',');
212 }
213
214 header_value.push_str(string);
215 }
216 }
217
218 Annotated(Some(HeaderValue::new(header_value)), meta)
219 }
220 annotated => LenientString::from_value(annotated).map_value(HeaderValue::new),
221 }
222 }
223}
224
225#[derive(Clone, Debug, Default, PartialEq, Empty, IntoValue, ProcessValue)]
227pub struct Headers(pub PairList<(Annotated<HeaderName>, Annotated<HeaderValue>)>);
228
229impl Headers {
230 pub fn get_header(&self, key: &str) -> Option<&str> {
231 for item in self.iter() {
232 if let Some((ref k, ref v)) = item.value() {
233 if k.as_str() == Some(key) {
234 return v.as_str();
235 }
236 }
237 }
238
239 None
240 }
241}
242
243impl std::ops::Deref for Headers {
244 type Target = PairList<(Annotated<HeaderName>, Annotated<HeaderValue>)>;
245
246 fn deref(&self) -> &Self::Target {
247 &self.0
248 }
249}
250
251impl std::ops::DerefMut for Headers {
252 fn deref_mut(&mut self) -> &mut Self::Target {
253 &mut self.0
254 }
255}
256
257impl FromValue for Headers {
258 fn from_value(value: Annotated<Value>) -> Annotated<Self> {
259 let should_sort = matches!(value.value(), Some(Value::Object(_)));
261
262 type HeaderTuple = (Annotated<HeaderName>, Annotated<HeaderValue>);
263 PairList::<HeaderTuple>::from_value(value).map_value(|mut pair_list| {
264 if should_sort {
265 pair_list.sort_unstable_by(|a, b| {
266 a.value()
267 .map(|x| x.0.value())
268 .cmp(&b.value().map(|x| x.0.value()))
269 });
270 }
271
272 Headers(pair_list)
273 })
274 }
275}
276
277#[derive(Clone, Debug, Default, PartialEq, Empty, IntoValue, ProcessValue)]
279pub struct Query(pub PairList<(Annotated<String>, Annotated<JsonLenientString>)>);
280
281impl Query {
282 pub fn parse(mut string: &str) -> Self {
283 if string.starts_with('?') {
284 string = &string[1..];
285 }
286
287 form_urlencoded::parse(string.as_bytes()).collect()
288 }
289}
290
291impl std::ops::Deref for Query {
292 type Target = PairList<(Annotated<String>, Annotated<JsonLenientString>)>;
293
294 fn deref(&self) -> &Self::Target {
295 &self.0
296 }
297}
298
299impl std::ops::DerefMut for Query {
300 fn deref_mut(&mut self) -> &mut Self::Target {
301 &mut self.0
302 }
303}
304
305impl<K, V> FromIterator<(K, V)> for Query
306where
307 K: Into<String>,
308 V: Into<String>,
309{
310 fn from_iter<T>(iter: T) -> Self
311 where
312 T: IntoIterator<Item = (K, V)>,
313 {
314 let pairs = iter.into_iter().map(|(key, value)| {
315 Annotated::new((
316 Annotated::new(key.into()),
317 Annotated::new(value.into().into()),
318 ))
319 });
320
321 Query(pairs.collect())
322 }
323}
324
325impl FromValue for Query {
326 fn from_value(value: Annotated<Value>) -> Annotated<Self> {
327 match value {
328 Annotated(Some(Value::String(v)), meta) => Annotated(Some(Query::parse(&v)), meta),
329 annotated @ Annotated(Some(Value::Object(_)), _)
330 | annotated @ Annotated(Some(Value::Array(_)), _) => {
331 PairList::from_value(annotated).map_value(Query)
332 }
333 Annotated(None, meta) => Annotated(None, meta),
334 Annotated(Some(value), mut meta) => {
335 meta.add_error(Error::expected("a query string or map"));
336 meta.set_original_value(Some(value));
337 Annotated(None, meta)
338 }
339 }
340 }
341}
342
343#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
402#[metastructure(process_func = "process_request", value_type = "Request")]
403pub struct Request {
404 #[metastructure(max_chars = 256, max_chars_allowance = 40, pii = "maybe")]
408 pub url: Annotated<String>,
409
410 pub method: Annotated<String>,
412
413 pub protocol: Annotated<String>,
415
416 #[metastructure(pii = "true", max_depth = 7, max_bytes = 8192)]
421 pub data: Annotated<Value>,
422
423 #[metastructure(pii = "true", max_depth = 3, max_bytes = 1024)]
430 #[metastructure(skip_serialization = "empty")]
431 pub query_string: Annotated<Query>,
432
433 #[metastructure(pii = "true", max_chars = 1024, max_chars_allowance = 100)]
435 #[metastructure(skip_serialization = "empty")]
436 pub fragment: Annotated<String>,
437
438 #[metastructure(pii = "true", max_depth = 5, max_bytes = 2048)]
442 #[metastructure(skip_serialization = "empty")]
443 pub cookies: Annotated<Cookies>,
444
445 #[metastructure(pii = "true", max_depth = 7, max_bytes = 8192)]
450 #[metastructure(skip_serialization = "empty")]
451 pub headers: Annotated<Headers>,
452
453 pub body_size: Annotated<u64>,
455
456 #[metastructure(pii = "true", max_depth = 7, max_bytes = 8192)]
463 #[metastructure(skip_serialization = "empty")]
464 pub env: Annotated<Object<Value>>,
465
466 #[metastructure(skip_serialization = "empty")]
468 pub inferred_content_type: Annotated<String>,
469
470 pub api_target: Annotated<String>,
478
479 #[metastructure(additional_properties, pii = "true")]
481 pub other: Object<Value>,
482}
483
484#[cfg(test)]
485mod tests {
486 use similar_asserts::assert_eq;
487
488 use super::*;
489
490 #[test]
491 fn test_header_normalization() {
492 let json = r#"{
493 "-other-": "header",
494 "accept": "application/json",
495 "WWW-Authenticate": "basic",
496 "x-sentry": "version=8"
497}"#;
498
499 let headers = vec![
500 Annotated::new((
501 Annotated::new("-Other-".to_string().into()),
502 Annotated::new("header".to_string().into()),
503 )),
504 Annotated::new((
505 Annotated::new("Accept".to_string().into()),
506 Annotated::new("application/json".to_string().into()),
507 )),
508 Annotated::new((
509 Annotated::new("WWW-Authenticate".to_string().into()),
510 Annotated::new("basic".to_string().into()),
511 )),
512 Annotated::new((
513 Annotated::new("X-Sentry".to_string().into()),
514 Annotated::new("version=8".to_string().into()),
515 )),
516 ];
517
518 let headers = Annotated::new(Headers(PairList(headers)));
519 assert_eq!(headers, Annotated::from_json(json).unwrap());
520 }
521
522 #[test]
523 fn test_header_from_sequence() {
524 let json = r#"[
525 ["accept", "application/json"]
526]"#;
527
528 let headers = vec![Annotated::new((
529 Annotated::new("Accept".to_string().into()),
530 Annotated::new("application/json".to_string().into()),
531 ))];
532
533 let headers = Annotated::new(Headers(PairList(headers)));
534 assert_eq!(headers, Annotated::from_json(json).unwrap());
535
536 let json = r#"[
537 ["accept", "application/json"],
538 [1, 2],
539 ["a", "b", "c"],
540 23
541]"#;
542 let headers = Annotated::<Headers>::from_json(json).unwrap();
543 #[derive(Debug, Empty, IntoValue)]
544 pub struct Container {
545 headers: Annotated<Headers>,
546 }
547 assert_eq!(
548 Annotated::new(Container { headers })
549 .to_json_pretty()
550 .unwrap(),
551 r#"{
552 "headers": [
553 [
554 "Accept",
555 "application/json"
556 ],
557 [
558 null,
559 "2"
560 ],
561 null,
562 null
563 ],
564 "_meta": {
565 "headers": {
566 "1": {
567 "0": {
568 "": {
569 "err": [
570 [
571 "invalid_data",
572 {
573 "reason": "expected a string"
574 }
575 ]
576 ],
577 "val": 1
578 }
579 }
580 },
581 "2": {
582 "": {
583 "err": [
584 [
585 "invalid_data",
586 {
587 "reason": "expected a tuple"
588 }
589 ]
590 ],
591 "val": [
592 "a",
593 "b",
594 "c"
595 ]
596 }
597 },
598 "3": {
599 "": {
600 "err": [
601 [
602 "invalid_data",
603 {
604 "reason": "expected a tuple"
605 }
606 ]
607 ],
608 "val": 23
609 }
610 }
611 }
612 }
613}"#
614 );
615 }
616
617 #[test]
618 fn test_request_roundtrip() {
619 let json = r#"{
620 "url": "https://google.com/search",
621 "method": "GET",
622 "data": {
623 "some": 1
624 },
625 "query_string": [
626 [
627 "q",
628 "foo"
629 ]
630 ],
631 "fragment": "home",
632 "cookies": [
633 [
634 "GOOGLE",
635 "1"
636 ]
637 ],
638 "headers": [
639 [
640 "Referer",
641 "https://google.com/"
642 ]
643 ],
644 "body_size": 1024,
645 "env": {
646 "REMOTE_ADDR": "213.47.147.207"
647 },
648 "inferred_content_type": "application/json",
649 "api_target": "graphql",
650 "other": "value"
651}"#;
652
653 let request = Annotated::new(Request {
654 url: Annotated::new("https://google.com/search".to_string()),
655 method: Annotated::new("GET".to_string()),
656 protocol: Annotated::empty(),
657 data: {
658 let mut map = Object::new();
659 map.insert("some".to_string(), Annotated::new(Value::I64(1)));
660 Annotated::new(Value::Object(map))
661 },
662 query_string: Annotated::new(Query(
663 vec![Annotated::new((
664 Annotated::new("q".to_string()),
665 Annotated::new("foo".to_string().into()),
666 ))]
667 .into(),
668 )),
669 fragment: Annotated::new("home".to_string()),
670 cookies: Annotated::new(Cookies({
671 PairList(vec![Annotated::new((
672 Annotated::new("GOOGLE".to_string()),
673 Annotated::new("1".to_string()),
674 ))])
675 })),
676 headers: Annotated::new(Headers({
677 let headers = vec![Annotated::new((
678 Annotated::new("Referer".to_string().into()),
679 Annotated::new("https://google.com/".to_string().into()),
680 ))];
681 PairList(headers)
682 })),
683 body_size: Annotated::new(1024),
684 env: Annotated::new({
685 let mut map = Object::new();
686 map.insert(
687 "REMOTE_ADDR".to_string(),
688 Annotated::new(Value::String("213.47.147.207".to_string())),
689 );
690 map
691 }),
692 inferred_content_type: Annotated::new("application/json".to_string()),
693 api_target: Annotated::new("graphql".to_string()),
694 other: {
695 let mut map = Object::new();
696 map.insert(
697 "other".to_string(),
698 Annotated::new(Value::String("value".to_string())),
699 );
700 map
701 },
702 });
703
704 assert_eq!(request, Annotated::from_json(json).unwrap());
705 assert_eq!(json, request.to_json_pretty().unwrap());
706 }
707
708 #[test]
709 fn test_query_string() {
710 let query = Annotated::new(Query(
711 vec![Annotated::new((
712 Annotated::new("foo".to_string()),
713 Annotated::new("bar".to_string().into()),
714 ))]
715 .into(),
716 ));
717 assert_eq!(query, Annotated::from_json("\"foo=bar\"").unwrap());
718 assert_eq!(query, Annotated::from_json("\"?foo=bar\"").unwrap());
719
720 let query = Annotated::new(Query(
721 vec![
722 Annotated::new((
723 Annotated::new("foo".to_string()),
724 Annotated::new("bar".to_string().into()),
725 )),
726 Annotated::new((
727 Annotated::new("baz".to_string()),
728 Annotated::new("42".to_string().into()),
729 )),
730 ]
731 .into(),
732 ));
733 assert_eq!(query, Annotated::from_json("\"foo=bar&baz=42\"").unwrap());
734 }
735
736 #[test]
737 fn test_query_string_legacy_nested() {
738 let query = Annotated::new(Query(
742 vec![Annotated::new((
743 Annotated::new("foo".to_string()),
744 Annotated::new("bar".to_string().into()),
745 ))]
746 .into(),
747 ));
748 assert_eq!(query, Annotated::from_json("\"foo=bar\"").unwrap());
749
750 let query = Annotated::new(Query(
751 vec![
752 Annotated::new((
753 Annotated::new("baz".to_string()),
754 Annotated::new(r#"{"a":42}"#.to_string().into()),
755 )),
756 Annotated::new((
757 Annotated::new("foo".to_string()),
758 Annotated::new("bar".to_string().into()),
759 )),
760 ]
761 .into(),
762 ));
763 assert_eq!(
764 query,
765 Annotated::from_json(
766 r#"
767 {
768 "foo": "bar",
769 "baz": {"a": 42}
770 }
771 "#
772 )
773 .unwrap()
774 );
775 }
776
777 #[test]
778 fn test_query_invalid() {
779 let query = Annotated::<Query>::from_error(
780 Error::expected("a query string or map"),
781 Some(Value::I64(42)),
782 );
783 assert_eq!(query, Annotated::from_json("42").unwrap());
784 }
785
786 #[test]
787 fn test_cookies_parsing() {
788 let json = "\" PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;\"";
789
790 let map = vec![
791 Annotated::new((
792 Annotated::new("PHPSESSID".to_string()),
793 Annotated::new("298zf09hf012fh2".to_string()),
794 )),
795 Annotated::new((
796 Annotated::new("csrftoken".to_string()),
797 Annotated::new("u32t4o3tb3gg43".to_string()),
798 )),
799 Annotated::new((
800 Annotated::new("_gat".to_string()),
801 Annotated::new("1".to_string()),
802 )),
803 ];
804
805 let cookies = Annotated::new(Cookies(PairList(map)));
806 assert_eq!(cookies, Annotated::from_json(json).unwrap());
807 }
808
809 #[test]
810 fn test_cookies_array() {
811 let input = r#"{"cookies":[["foo","bar"],["invalid", 42],["none",null]]}"#;
812 let output = r#"{"cookies":[["foo","bar"],["invalid",null],["none",null]],"_meta":{"cookies":{"1":{"1":{"":{"err":[["invalid_data",{"reason":"expected a string"}]],"val":42}}}}}}"#;
813
814 let map = vec![
815 Annotated::new((
816 Annotated::new("foo".to_string()),
817 Annotated::new("bar".to_string()),
818 )),
819 Annotated::new((
820 Annotated::new("invalid".to_string()),
821 Annotated::from_error(Error::expected("a string"), Some(Value::I64(42))),
822 )),
823 Annotated::new((Annotated::new("none".to_string()), Annotated::empty())),
824 ];
825
826 let cookies = Annotated::new(Cookies(PairList(map)));
827 let request = Annotated::new(Request {
828 cookies,
829 ..Default::default()
830 });
831 assert_eq!(request, Annotated::from_json(input).unwrap());
832 assert_eq!(request.to_json().unwrap(), output);
833 }
834
835 #[test]
836 fn test_cookies_object() {
837 let json = r#"{"foo":"bar", "invalid": 42}"#;
838
839 let map = vec![
840 Annotated::new((
841 Annotated::new("foo".to_string()),
842 Annotated::new("bar".to_string()),
843 )),
844 Annotated::new((
845 Annotated::new("invalid".to_string()),
846 Annotated::from_error(Error::expected("a string"), Some(Value::I64(42))),
847 )),
848 ];
849
850 let cookies = Annotated::new(Cookies(PairList(map)));
851 assert_eq!(cookies, Annotated::from_json(json).unwrap());
852 }
853
854 #[test]
855 fn test_cookies_invalid() {
856 let cookies =
857 Annotated::<Cookies>::from_error(Error::expected("cookies"), Some(Value::I64(42)));
858 assert_eq!(cookies, Annotated::from_json("42").unwrap());
859 }
860
861 #[test]
862 fn test_querystring_without_value() {
863 let json = r#""foo=bar&baz""#;
864
865 let query = Annotated::new(Query(
866 vec![
867 Annotated::new((
868 Annotated::new("foo".to_string()),
869 Annotated::new("bar".to_string().into()),
870 )),
871 Annotated::new((
872 Annotated::new("baz".to_string()),
873 Annotated::new("".to_string().into()),
874 )),
875 ]
876 .into(),
877 ));
878
879 assert_eq!(query, Annotated::from_json(json).unwrap());
880 }
881
882 #[test]
883 fn test_headers_lenient_value() {
884 let input = r#"{
885 "headers": {
886 "X-Foo": "",
887 "X-Bar": 42
888 }
889}"#;
890
891 let output = r#"{
892 "headers": [
893 [
894 "X-Bar",
895 "42"
896 ],
897 [
898 "X-Foo",
899 ""
900 ]
901 ]
902}"#;
903
904 let request = Annotated::new(Request {
905 headers: Annotated::new(Headers(PairList(vec![
906 Annotated::new((
907 Annotated::new("X-Bar".to_string().into()),
908 Annotated::new("42".to_string().into()),
909 )),
910 Annotated::new((
911 Annotated::new("X-Foo".to_string().into()),
912 Annotated::new("".to_string().into()),
913 )),
914 ]))),
915 ..Default::default()
916 });
917
918 assert_eq!(Annotated::from_json(input).unwrap(), request);
919 assert_eq!(request.to_json_pretty().unwrap(), output);
920 }
921
922 #[test]
923 fn test_headers_multiple_values() {
924 let input = r#"{
925 "headers": {
926 "X-Foo": [""],
927 "X-Bar": [
928 42,
929 "bar",
930 "baz"
931 ]
932 }
933}"#;
934
935 let output = r#"{
936 "headers": [
937 [
938 "X-Bar",
939 "42,bar,baz"
940 ],
941 [
942 "X-Foo",
943 ""
944 ]
945 ]
946}"#;
947
948 let request = Annotated::new(Request {
949 headers: Annotated::new(Headers(PairList(vec![
950 Annotated::new((
951 Annotated::new("X-Bar".to_string().into()),
952 Annotated::new("42,bar,baz".to_string().into()),
953 )),
954 Annotated::new((
955 Annotated::new("X-Foo".to_string().into()),
956 Annotated::new("".to_string().into()),
957 )),
958 ]))),
959 ..Default::default()
960 });
961
962 assert_eq!(Annotated::from_json(input).unwrap(), request);
963 assert_eq!(request.to_json_pretty().unwrap(), output);
964 }
965}