ic_http_certification/hash/
request_hash.rs1use super::Hash;
2use crate::{cel::DefaultRequestCertification, HttpCertificationResult, HttpRequest};
3use ic_representation_independent_hash::{hash, representation_independent_hash, Value};
4
5pub fn request_hash<'a>(
9 request: &'a HttpRequest,
10 request_certification: &'a DefaultRequestCertification,
11) -> HttpCertificationResult<Hash> {
12 let mut filtered_headers = get_filtered_headers(request.headers(), request_certification);
13
14 filtered_headers.push((
15 ":ic-cert-method".into(),
16 Value::String(request.method().to_string()),
17 ));
18
19 let filtered_query = request
20 .get_query()?
21 .and_then(|query| get_filtered_query(&query, request_certification));
22 if let Some(query_hash) = filtered_query {
23 filtered_headers.push((":ic-cert-query".into(), Value::String(query_hash)))
24 }
25
26 let concatenated_hashes = [
27 representation_independent_hash(&filtered_headers),
28 hash(request.body()),
29 ]
30 .concat();
31
32 Ok(hash(concatenated_hashes.as_slice()))
33}
34
35fn get_filtered_headers(
36 headers: &[(String, String)],
37 request_certification: &DefaultRequestCertification,
38) -> Vec<(String, Value)> {
39 headers
40 .iter()
41 .filter_map(|(header_name, header_value)| {
42 let is_header_included =
43 request_certification
44 .headers
45 .iter()
46 .any(|header_to_include| {
47 header_to_include.eq_ignore_ascii_case(&header_name.to_string())
48 });
49
50 if !is_header_included {
51 return None;
52 }
53
54 Some((
55 header_name.to_string().to_ascii_lowercase(),
56 Value::String(String::from(header_value)),
57 ))
58 })
59 .collect()
60}
61
62fn get_filtered_query(
63 query: &str,
64 request_certification: &DefaultRequestCertification,
65) -> Option<String> {
66 let filtered_query_string = query
67 .split('&')
68 .filter(|query_fragment| {
69 let mut split_fragment: Vec<&str> = query_fragment.split('=').take(1).collect();
70 let query_param_name = split_fragment.pop();
71
72 query_param_name
73 .map(|query_param_name| {
74 request_certification
75 .query_parameters
76 .iter()
77 .any(|query_param_to_include| {
78 query_param_to_include.eq_ignore_ascii_case(query_param_name)
79 })
80 })
81 .unwrap_or(false)
82 })
83 .collect::<Vec<_>>();
84 if filtered_query_string.is_empty() {
85 return None;
86 }
87
88 Some(filtered_query_string.join("&"))
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn request_hash_without_query() {
97 let request_certification = DefaultRequestCertification::new(vec!["host"], vec![]);
98 let request = create_request("https://ic0.app");
99 let expected_hash =
100 hex::decode("10796453466efb3e333891136b8a5931269f77e40ead9d437fcee94a02fa833c")
101 .unwrap();
102
103 let result = request_hash(&request, &request_certification).unwrap();
104
105 assert_eq!(result, expected_hash.as_slice());
106 }
107
108 #[test]
109 fn request_hash_with_uncertified_query() {
110 let request_certification = DefaultRequestCertification::new(vec!["host"], vec![]);
111 let request = create_request("https://ic0.app?q=search");
112 let expected_hash =
113 hex::decode("10796453466efb3e333891136b8a5931269f77e40ead9d437fcee94a02fa833c")
114 .unwrap();
115
116 let result = request_hash(&request, &request_certification).unwrap();
117
118 assert_eq!(result, expected_hash.as_slice());
119 }
120
121 #[test]
122 fn request_hash_with_query() {
123 let request_certification =
124 DefaultRequestCertification::new(vec!["host"], vec!["q", "name"]);
125 let request =
126 create_request("https://ic0.app?q=hello+world&name=foo&name=bar&color=purple");
127 let expected_hash =
128 hex::decode("3ade1c9054f05bc8bcebd3fd7b884078a6e67c63e5ac4a639fa46a47f5a955c9")
129 .unwrap();
130
131 let result = request_hash(&request, &request_certification).unwrap();
132
133 assert_eq!(result, expected_hash.as_slice());
134 }
135
136 #[test]
137 fn request_hash_query_order_matters() {
138 let request_certification =
139 DefaultRequestCertification::new(vec!["host"], vec!["q", "name"]);
140 let request =
141 create_request("https://ic0.app?q=hello+world&name=foo&name=bar&color=purple");
142 let reordered_request =
143 create_request("https://ic0.app?q=hello+world&name=bar&name=foo&color=purple");
144
145 let result = request_hash(&request, &request_certification).unwrap();
146 let reordered_result = request_hash(&reordered_request, &request_certification).unwrap();
147
148 assert_ne!(result, reordered_result);
149 }
150
151 #[test]
152 fn request_hash_query_with_fragment_does_not_change() {
153 let request_certification =
154 DefaultRequestCertification::new(vec!["host"], vec!["q", "name"]);
155 let request =
156 create_request("https://ic0.app?q=hello+world&name=foo&name=bar&color=purple");
157 let request_with_fragment = create_request(
158 "https://ic0.app?q=hello+world&name=foo&name=bar&color=purple#index.html",
159 );
160
161 let result = request_hash(&request, &request_certification).unwrap();
162 let result_with_fragment =
163 request_hash(&request_with_fragment, &request_certification).unwrap();
164
165 assert_eq!(result, result_with_fragment);
166 }
167
168 fn create_request(uri: &str) -> HttpRequest {
169 HttpRequest::post(uri)
170 .with_headers(vec![
171 ("Accept-Language".into(), "en".into()),
172 ("Accept-Language".into(), "en-US".into()),
173 ("Host".into(), "https://ic0.app".into()),
174 ])
175 .with_body(vec![0, 1, 2, 3, 4, 5, 6])
176 .build()
177 }
178}