ic_http_certification/hash/
request_hash.rs

1use super::Hash;
2use crate::{cel::DefaultRequestCertification, HttpCertificationResult, HttpRequest};
3use ic_representation_independent_hash::{hash, representation_independent_hash, Value};
4
5/// Calculates the
6/// [Representation Independent Hash](https://internetcomputer.org/docs/current/references/ic-interface-spec/#hash-of-map)
7/// of an [HttpRequest] according to a CEL expression defined by [DefaultRequestCertification].
8pub 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}