#[cfg(test)]
mod tests;
use crate::{
common::{App, UserCounting},
configuration::Config,
cup_ecdsa::{CupDecorationError, CupRequest, Cupv2RequestHandler, RequestMetadata},
protocol::{
PROTOCOL_V3,
request::{
Event, GUID, HEADER_APP_ID, HEADER_INTERACTIVITY, HEADER_UPDATER_NAME, InstallSource,
Ping, Request, RequestWrapper, UpdateCheck,
},
},
};
use http;
use log::{info, warn};
use std::fmt::Display;
use std::result;
use thiserror::Error;
type ProtocolApp = crate::protocol::request::App;
#[derive(Debug, Error)]
pub enum Error {
#[error("Unexpected JSON error constructing update check")]
Json(#[from] serde_json::Error),
#[error("Http error performing update check")]
Http(#[from] http::Error),
#[error("Error decorating outgoing request with CUPv2 parameters")]
Cup(#[from] CupDecorationError),
}
pub type Result<T> = result::Result<T, Error>;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct RequestParams {
pub source: InstallSource,
pub use_configured_proxies: bool,
pub disable_updates: bool,
pub offer_update_if_same_version: bool,
}
#[derive(Clone)]
struct AppEntry {
app: App,
update_check: Option<UpdateCheck>,
ping: bool,
events: Vec<Event>,
}
impl AppEntry {
fn new(app: &App) -> AppEntry {
AppEntry {
app: app.clone(),
update_check: None,
ping: false,
events: Vec::new(),
}
}
}
impl From<AppEntry> for ProtocolApp {
fn from(entry: AppEntry) -> ProtocolApp {
if entry.update_check.is_none() && entry.events.is_empty() && !entry.ping {
warn!(
"Generated protocol::request for {} has no update check, ping, or events",
entry.app.id
);
}
let ping = if entry.ping {
let UserCounting::ClientRegulatedByDate(days) = entry.app.user_counting;
Some(Ping {
date_last_active: days,
date_last_roll_call: days,
})
} else {
None
};
ProtocolApp {
id: entry.app.id,
version: entry.app.version.to_string(),
fingerprint: entry.app.fingerprint,
cohort: Some(entry.app.cohort),
update_check: entry.update_check,
events: entry.events,
ping,
extra_fields: entry.app.extra_fields,
}
}
}
pub struct RequestBuilder<'a> {
config: &'a Config,
params: RequestParams,
app_entries: Vec<AppEntry>,
request_id: Option<GUID>,
session_id: Option<GUID>,
}
impl<'a> RequestBuilder<'a> {
pub fn new(config: &'a Config, params: &RequestParams) -> Self {
RequestBuilder {
config,
params: params.clone(),
app_entries: Vec::new(),
request_id: None,
session_id: None,
}
}
fn insert_and_modify_entry<F>(&mut self, app: &App, modify: F)
where
F: FnOnce(&mut AppEntry),
{
if let Some(app_entry) = self.app_entries.iter_mut().find(|e| e.app.id == app.id) {
modify(app_entry);
} else {
let mut app_entry = AppEntry::new(app);
modify(&mut app_entry);
self.app_entries.push(app_entry);
}
}
pub fn add_update_check(mut self, app: &App) -> Self {
let update_check = UpdateCheck {
disabled: self.params.disable_updates,
offer_update_if_same_version: self.params.offer_update_if_same_version,
};
self.insert_and_modify_entry(app, |entry| {
entry.update_check = Some(update_check);
});
self
}
pub fn add_ping(mut self, app: &App) -> Self {
self.insert_and_modify_entry(app, |entry| {
entry.ping = true;
});
self
}
pub fn add_event(mut self, app: &App, event: Event) -> Self {
self.insert_and_modify_entry(app, |entry| {
entry.events.push(event);
});
self
}
pub fn request_id(self, request_id: GUID) -> Self {
Self {
request_id: Some(request_id),
..self
}
}
pub fn session_id(self, session_id: GUID) -> Self {
Self {
session_id: Some(session_id),
..self
}
}
pub fn build(
&self,
cup_handler: Option<&impl Cupv2RequestHandler>,
) -> Result<(http::Request<hyper::Body>, Option<RequestMetadata>)> {
let (intermediate, request_metadata) = self.build_intermediate(cup_handler)?;
if self
.app_entries
.iter()
.any(|app| app.update_check.is_some())
{
info!("Building Request: {}", intermediate);
}
Ok((
Into::<Result<http::Request<hyper::Body>>>::into(intermediate)?,
request_metadata,
))
}
fn build_intermediate(
&self,
cup_handler: Option<&impl Cupv2RequestHandler>,
) -> Result<(Intermediate, Option<RequestMetadata>)> {
let mut headers = vec![
(
http::header::CONTENT_TYPE.as_str(),
"application/json".to_string(),
),
(HEADER_UPDATER_NAME, self.config.updater.name.clone()),
(
HEADER_INTERACTIVITY,
match self.params.source {
InstallSource::OnDemand => "fg".to_string(),
InstallSource::ScheduledTask => "bg".to_string(),
},
),
];
if let Some(main_app) = self.app_entries.first() {
headers.push((HEADER_APP_ID, main_app.app.id.clone()));
}
let apps = self
.app_entries
.iter()
.cloned()
.map(ProtocolApp::from)
.collect();
let mut intermediate = Intermediate {
uri: self.config.service_url.clone(),
headers,
body: RequestWrapper {
request: Request {
protocol_version: PROTOCOL_V3.to_string(),
updater: self.config.updater.name.clone(),
updater_version: self.config.updater.version.to_string(),
install_source: self.params.source,
is_machine: true,
request_id: self.request_id.clone(),
session_id: self.session_id.clone(),
os: self.config.os.clone(),
apps,
},
},
};
let request_metadata = match cup_handler.as_ref() {
Some(handler) => Some(handler.decorate_request(&mut intermediate)?),
_ => None,
};
Ok((intermediate, request_metadata))
}
}
#[derive(Debug)]
pub struct Intermediate {
pub uri: String,
pub headers: Vec<(&'static str, String)>,
pub body: RequestWrapper,
}
impl Intermediate {
pub fn serialize_body(&self) -> serde_json::Result<Vec<u8>> {
serde_json::to_vec(&self.body)
}
}
impl Display for Intermediate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "uri: {} ", self.uri)?;
for (name, value) in &self.headers {
writeln!(f, "header: {name}={value}")?;
}
match serde_json::to_value(&self.body) {
Ok(value) => writeln!(f, "body: {value:#}"),
Err(e) => writeln!(f, "err: {e}"),
}
}
}
impl From<Intermediate> for Result<http::Request<hyper::Body>> {
fn from(intermediate: Intermediate) -> Self {
let mut builder = hyper::Request::post(&intermediate.uri);
for (key, value) in &intermediate.headers {
builder = builder.header(*key, value);
}
let request = builder.body(intermediate.serialize_body()?.into())?;
Ok(request)
}
}
impl CupRequest for Intermediate {
fn get_uri(&self) -> &str {
&self.uri
}
fn set_uri(&mut self, uri: String) {
self.uri = uri;
}
fn get_serialized_body(&self) -> serde_json::Result<Vec<u8>> {
self.serialize_body()
}
}