1use std::collections::HashMap;
8
9use chrono::prelude::{DateTime, Utc};
10use flate2::{read::GzDecoder, write::GzEncoder, Compression};
11use serde_json::Value as JsonValue;
12use std::io::prelude::*;
13
14use crate::error::{ErrorKind, Result};
15use crate::system;
16
17pub type HeaderMap = HashMap<String, String>;
19
20pub(crate) fn create_date_header_value(current_time: DateTime<Utc>) -> String {
22 current_time.format("%a, %d %b %Y %T GMT").to_string()
33}
34
35fn create_x_telemetry_agent_header_value(
36 version: &str,
37 language_binding_name: &str,
38 system: &str,
39) -> String {
40 format!(
41 "Glean/{} ({} on {})",
42 version, language_binding_name, system
43 )
44}
45
46fn gzip_content(path: &str, content: &[u8]) -> Option<Vec<u8>> {
48 let mut gzipper = GzEncoder::new(Vec::new(), Compression::default());
49
50 if let Err(e) = gzipper.write_all(content) {
52 log::warn!("Failed to write to the gzipper: {} - {:?}", path, e);
53 return None;
54 }
55
56 gzipper.finish().ok()
57}
58
59pub struct Builder {
60 document_id: Option<String>,
61 path: Option<String>,
62 body: Option<Vec<u8>>,
63 headers: HeaderMap,
64 body_max_size: usize,
65 body_has_info_sections: Option<bool>,
66 ping_name: Option<String>,
67 uploader_capabilities: Option<Vec<String>>,
68}
69
70impl Builder {
71 pub fn new(language_binding_name: &str, body_max_size: usize) -> Self {
73 let mut headers = HashMap::new();
74 headers.insert(
75 "X-Telemetry-Agent".to_string(),
76 create_x_telemetry_agent_header_value(
77 crate::GLEAN_VERSION,
78 language_binding_name,
79 system::OS,
80 ),
81 );
82 headers.insert(
83 "Content-Type".to_string(),
84 "application/json; charset=utf-8".to_string(),
85 );
86
87 Self {
88 document_id: None,
89 path: None,
90 body: None,
91 headers,
92 body_max_size,
93 body_has_info_sections: None,
94 ping_name: None,
95 uploader_capabilities: None,
96 }
97 }
98
99 pub fn document_id<S: Into<String>>(mut self, value: S) -> Self {
101 self.document_id = Some(value.into());
102 self
103 }
104
105 pub fn path<S: Into<String>>(mut self, value: S) -> Self {
107 self.path = Some(value.into());
108 self
109 }
110
111 pub fn body<S: Into<String>>(mut self, value: S) -> Self {
126 let original_as_string = value.into();
128 let gzipped_content = gzip_content(
129 self.path
130 .as_ref()
131 .expect("Path must be set before attempting to set the body"),
132 original_as_string.as_bytes(),
133 );
134 let add_gzip_header = gzipped_content.is_some();
135 let body = gzipped_content.unwrap_or_else(|| original_as_string.into_bytes());
136
137 self = self.header("Content-Length", &body.len().to_string());
139 if add_gzip_header {
140 self = self.header("Content-Encoding", "gzip");
141 }
142
143 self.body = Some(body);
144 self
145 }
146
147 pub fn body_has_info_sections(mut self, body_has_info_sections: bool) -> Self {
149 self.body_has_info_sections = Some(body_has_info_sections);
150 self
151 }
152
153 pub fn ping_name<S: Into<String>>(mut self, ping_name: S) -> Self {
155 self.ping_name = Some(ping_name.into());
156 self
157 }
158
159 pub fn header<S: Into<String>>(mut self, key: S, value: S) -> Self {
161 self.headers.insert(key.into(), value.into());
162 self
163 }
164
165 pub fn headers(mut self, values: HeaderMap) -> Self {
167 self.headers.extend(values);
168 self
169 }
170
171 pub fn uploader_capabilities(mut self, uploader_capabilities: Vec<String>) -> Self {
173 self.uploader_capabilities = Some(uploader_capabilities);
174 self
175 }
176
177 pub fn build(self) -> Result<PingRequest> {
184 let body = self
185 .body
186 .expect("body must be set before attempting to build PingRequest");
187
188 if body.len() > self.body_max_size {
189 return Err(ErrorKind::PingBodyOverflow(body.len()).into());
190 }
191
192 Ok(PingRequest {
193 document_id: self
194 .document_id
195 .expect("document_id must be set before attempting to build PingRequest"),
196 path: self
197 .path
198 .expect("path must be set before attempting to build PingRequest"),
199 body,
200 headers: self.headers,
201 body_has_info_sections: self.body_has_info_sections.expect(
202 "body_has_info_sections must be set before attempting to build PingRequest",
203 ),
204 ping_name: self
205 .ping_name
206 .expect("ping_name must be set before attempting to build PingRequest"),
207 uploader_capabilities: self
208 .uploader_capabilities
209 .expect("uploader_capabilities must be set before attempting to build PingRequest"),
210 })
211 }
212}
213
214#[derive(PartialEq, Eq, Debug, Clone)]
216pub struct PingRequest {
217 pub document_id: String,
220 pub path: String,
222 pub body: Vec<u8>,
226 pub headers: HeaderMap,
228 pub body_has_info_sections: bool,
230 pub ping_name: String,
232 pub uploader_capabilities: Vec<String>,
234}
235
236impl PingRequest {
237 pub fn builder(language_binding_name: &str, body_max_size: usize) -> Builder {
245 Builder::new(language_binding_name, body_max_size)
246 }
247
248 pub fn is_deletion_request(&self) -> bool {
250 self.ping_name == "deletion-request"
251 }
252
253 pub fn pretty_body(&self) -> Option<String> {
258 let mut gz = GzDecoder::new(&self.body[..]);
259 let mut s = String::with_capacity(self.body.len());
260
261 gz.read_to_string(&mut s)
262 .ok()
263 .map(|_| &s[..])
264 .or_else(|| std::str::from_utf8(&self.body).ok())
265 .and_then(|payload| serde_json::from_str::<JsonValue>(payload).ok())
266 .and_then(|json| serde_json::to_string_pretty(&json).ok())
267 }
268}
269
270#[cfg(test)]
271mod test {
272 use super::*;
273 use chrono::offset::TimeZone;
274
275 #[test]
276 fn date_header_resolution() {
277 let date: DateTime<Utc> = Utc.with_ymd_and_hms(2018, 2, 25, 11, 10, 37).unwrap();
278 let test_value = create_date_header_value(date);
279 assert_eq!("Sun, 25 Feb 2018 11:10:37 GMT", test_value);
280 }
281
282 #[test]
283 fn x_telemetry_agent_header_resolution() {
284 let test_value = create_x_telemetry_agent_header_value("0.0.0", "Rust", "Windows");
285 assert_eq!("Glean/0.0.0 (Rust on Windows)", test_value);
286 }
287
288 #[test]
289 fn correctly_builds_ping_request() {
290 let request = PingRequest::builder("Rust", 1024 * 1024)
291 .document_id("woop")
292 .path("/random/path/doesnt/matter")
293 .body("{}")
294 .body_has_info_sections(false)
295 .ping_name("whatevs")
296 .uploader_capabilities(vec![])
297 .build()
298 .unwrap();
299
300 assert_eq!(request.document_id, "woop");
301 assert_eq!(request.path, "/random/path/doesnt/matter");
302 assert!(!request.body_has_info_sections);
303 assert_eq!(request.ping_name, "whatevs");
304
305 assert!(request.headers.contains_key("X-Telemetry-Agent"));
307 assert!(request.headers.contains_key("Content-Type"));
308 assert!(request.headers.contains_key("Content-Length"));
309
310 }
313
314 #[test]
315 fn errors_when_request_body_exceeds_max_size() {
316 let request = Builder::new(
319 "Rust", 1,
320 )
321 .document_id("woop")
322 .path("/random/path/doesnt/matter")
323 .body("{}")
324 .build();
325
326 assert!(request.is_err());
327 }
328}