use std::collections::HashMap;
use chrono::prelude::{DateTime, Utc};
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use serde_json::Value as JsonValue;
use std::io::prelude::*;
use crate::error::{ErrorKind, Result};
use crate::system;
pub type HeaderMap = HashMap<String, String>;
pub(crate) fn create_date_header_value(current_time: DateTime<Utc>) -> String {
current_time.format("%a, %d %b %Y %T GMT").to_string()
}
fn create_x_telemetry_agent_header_value(
version: &str,
language_binding_name: &str,
system: &str,
) -> String {
format!(
"Glean/{} ({} on {})",
version, language_binding_name, system
)
}
fn gzip_content(path: &str, content: &[u8]) -> Option<Vec<u8>> {
let mut gzipper = GzEncoder::new(Vec::new(), Compression::default());
if let Err(e) = gzipper.write_all(content) {
log::warn!("Failed to write to the gzipper: {} - {:?}", path, e);
return None;
}
gzipper.finish().ok()
}
pub struct Builder {
document_id: Option<String>,
path: Option<String>,
body: Option<Vec<u8>>,
headers: HeaderMap,
body_max_size: usize,
body_has_info_sections: Option<bool>,
ping_name: Option<String>,
uploader_capabilities: Option<Vec<String>>,
}
impl Builder {
pub fn new(language_binding_name: &str, body_max_size: usize) -> Self {
let mut headers = HashMap::new();
headers.insert(
"X-Telemetry-Agent".to_string(),
create_x_telemetry_agent_header_value(
crate::GLEAN_VERSION,
language_binding_name,
system::OS,
),
);
headers.insert(
"Content-Type".to_string(),
"application/json; charset=utf-8".to_string(),
);
Self {
document_id: None,
path: None,
body: None,
headers,
body_max_size,
body_has_info_sections: None,
ping_name: None,
uploader_capabilities: None,
}
}
pub fn document_id<S: Into<String>>(mut self, value: S) -> Self {
self.document_id = Some(value.into());
self
}
pub fn path<S: Into<String>>(mut self, value: S) -> Self {
self.path = Some(value.into());
self
}
pub fn body<S: Into<String>>(mut self, value: S) -> Self {
let original_as_string = value.into();
let gzipped_content = gzip_content(
self.path
.as_ref()
.expect("Path must be set before attempting to set the body"),
original_as_string.as_bytes(),
);
let add_gzip_header = gzipped_content.is_some();
let body = gzipped_content.unwrap_or_else(|| original_as_string.into_bytes());
self = self.header("Content-Length", &body.len().to_string());
if add_gzip_header {
self = self.header("Content-Encoding", "gzip");
}
self.body = Some(body);
self
}
pub fn body_has_info_sections(mut self, body_has_info_sections: bool) -> Self {
self.body_has_info_sections = Some(body_has_info_sections);
self
}
pub fn ping_name<S: Into<String>>(mut self, ping_name: S) -> Self {
self.ping_name = Some(ping_name.into());
self
}
pub fn header<S: Into<String>>(mut self, key: S, value: S) -> Self {
self.headers.insert(key.into(), value.into());
self
}
pub fn headers(mut self, values: HeaderMap) -> Self {
self.headers.extend(values);
self
}
pub fn uploader_capabilities(mut self, uploader_capabilities: Vec<String>) -> Self {
self.uploader_capabilities = Some(uploader_capabilities);
self
}
pub fn build(self) -> Result<PingRequest> {
let body = self
.body
.expect("body must be set before attempting to build PingRequest");
if body.len() > self.body_max_size {
return Err(ErrorKind::PingBodyOverflow(body.len()).into());
}
Ok(PingRequest {
document_id: self
.document_id
.expect("document_id must be set before attempting to build PingRequest"),
path: self
.path
.expect("path must be set before attempting to build PingRequest"),
body,
headers: self.headers,
body_has_info_sections: self.body_has_info_sections.expect(
"body_has_info_sections must be set before attempting to build PingRequest",
),
ping_name: self
.ping_name
.expect("ping_name must be set before attempting to build PingRequest"),
uploader_capabilities: self
.uploader_capabilities
.expect("uploader_capabilities must be set before attempting to build PingRequest"),
})
}
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct PingRequest {
pub document_id: String,
pub path: String,
pub body: Vec<u8>,
pub headers: HeaderMap,
pub body_has_info_sections: bool,
pub ping_name: String,
pub uploader_capabilities: Vec<String>,
}
impl PingRequest {
pub fn builder(language_binding_name: &str, body_max_size: usize) -> Builder {
Builder::new(language_binding_name, body_max_size)
}
pub fn is_deletion_request(&self) -> bool {
self.ping_name == "deletion-request"
}
pub fn pretty_body(&self) -> Option<String> {
let mut gz = GzDecoder::new(&self.body[..]);
let mut s = String::with_capacity(self.body.len());
gz.read_to_string(&mut s)
.ok()
.map(|_| &s[..])
.or_else(|| std::str::from_utf8(&self.body).ok())
.and_then(|payload| serde_json::from_str::<JsonValue>(payload).ok())
.and_then(|json| serde_json::to_string_pretty(&json).ok())
}
}
#[cfg(test)]
mod test {
use super::*;
use chrono::offset::TimeZone;
#[test]
fn date_header_resolution() {
let date: DateTime<Utc> = Utc.with_ymd_and_hms(2018, 2, 25, 11, 10, 37).unwrap();
let test_value = create_date_header_value(date);
assert_eq!("Sun, 25 Feb 2018 11:10:37 GMT", test_value);
}
#[test]
fn x_telemetry_agent_header_resolution() {
let test_value = create_x_telemetry_agent_header_value("0.0.0", "Rust", "Windows");
assert_eq!("Glean/0.0.0 (Rust on Windows)", test_value);
}
#[test]
fn correctly_builds_ping_request() {
let request = PingRequest::builder( "Rust", 1024 * 1024)
.document_id("woop")
.path("/random/path/doesnt/matter")
.body("{}")
.body_has_info_sections(false)
.ping_name("whatevs")
.uploader_capabilities(vec![])
.build()
.unwrap();
assert_eq!(request.document_id, "woop");
assert_eq!(request.path, "/random/path/doesnt/matter");
assert!(!request.body_has_info_sections);
assert_eq!(request.ping_name, "whatevs");
assert!(request.headers.contains_key("X-Telemetry-Agent"));
assert!(request.headers.contains_key("Content-Type"));
assert!(request.headers.contains_key("Content-Length"));
}
#[test]
fn errors_when_request_body_exceeds_max_size() {
let request = Builder::new(
"Rust", 1,
)
.document_id("woop")
.path("/random/path/doesnt/matter")
.body("{}")
.build();
assert!(request.is_err());
}
}