blob: 692ac5780173e4bb51df2a31131b816606a5c601 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#[cfg(test)]
mod tests;
use crate::{
common::{App, UserCounting},
configuration::Config,
protocol::{
request::{
Event, InstallSource, Ping, Request, RequestWrapper, UpdateCheck, GUID, HEADER_APP_ID,
HEADER_INTERACTIVITY, HEADER_UPDATER_NAME,
},
PROTOCOL_V3,
},
};
use http;
use log::*;
use std::fmt::Display;
use std::result;
use thiserror::Error;
type ProtocolApp = crate::protocol::request::App;
/// Building a request can fail for multiple reasons, this enum consolidates them into a single
/// type that can be used to express those reasons.
#[derive(Debug, Error)]
pub enum Error {
#[error("Unexpected JSON error constructing update check: {}", _0)]
Json(serde_json::Error),
#[error("Http error performing update check: {}", _0)]
Http(http::Error),
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Error::Json(e)
}
}
impl From<http::Error> for Error {
fn from(e: http::Error) -> Self {
Error::Http(e)
}
}
/// The builder's own Result type.
pub type Result<T> = result::Result<T, Error>;
/// These are the parameters that describe how the request should be performed.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct RequestParams {
/// The install source for a request changes a number of properties of the request, including
/// the HTTP request headers, and influences how Omaha services the request (e.g. throttling)
pub source: InstallSource,
/// If true, the request should use any configured proxies. This allows the bypassing of
/// proxies if there are difficulties in communicating with the Omaha service.
pub use_configured_proxies: bool,
}
/// The AppEntry holds the data for the app whose request is currently being constructed. An app
/// can only have a single cohort, update check, or ping, but may have multiple events. Note that
/// while this object allows for no update check, no ping, and no events, that doesn't make sense
/// via the protocol.
///
/// This struct has ownership over it's members, so that they may later be moved out when the
/// request itself is built.
#[derive(Clone)]
struct AppEntry {
/// The identifying data for the application.
app: App,
/// Set to true if an update check should be performed.
update_check: bool,
/// Set to true if a ping should be send.
ping: bool,
/// Any events that need to be sent to the Omaha service.
events: Vec<Event>,
}
impl AppEntry {
/// Basic constructor for the AppEntry. All AppEntries MUST have an App and a Cohort,
/// everything else can be omitted.
fn new(app: &App) -> AppEntry {
AppEntry { app: app.clone(), update_check: false, ping: false, events: Vec::new() }
}
}
/// Conversion method to construct a ProtocolApp from an AppEntry. This consumes the entry, moving
/// it's members into the generated ProtocolApp.
impl From<AppEntry> for ProtocolApp {
fn from(entry: AppEntry) -> ProtocolApp {
if !entry.update_check && 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 days = match entry.app.user_counting {
UserCounting::ClientRegulatedByDate(days) => days,
};
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: if entry.update_check { Some(UpdateCheck::default()) } else { None },
events: entry.events,
ping,
extra_fields: entry.app.extra_fields,
}
}
}
/// The RequestBuilder is used to create the protocol requests. Each request is represented by an
/// instance of protocol::request::Request.
pub struct RequestBuilder<'a> {
// The static data identifying the updater binary.
config: &'a Config,
// The parameters that control how this request is to be made.
params: RequestParams,
// The applications to include in this request, with their associated update checks, pings, and
// events to report.
app_entries: Vec<AppEntry>,
request_id: Option<GUID>,
session_id: Option<GUID>,
}
/// The RequestBuilder is a stateful builder for protocol::request::Request objects. After being
/// instantiated with the base parameters for the current request, it has functions for accumulating
/// an update check, a ping, and multiple events for individual App objects.
///
/// The 'add_*()' functions are all insensitive to order for a given App and it's Cohort. However,
/// if multiple different App entries are used, then order matters. The order in the request is
/// the order that the Apps are added to the RequestBuilder.
///
/// Further, the cohort is only captured on the _first_ time a given App is added to the request.
/// If, for some reason, the same App is added twice, but with a different cohort, the latter cohort
/// is ignored.
///
/// The operation being added (update check, ping, or event) is added to the existing App. The app
/// maintains its existing place in the list of Apps to be added to the request.
impl<'a> RequestBuilder<'a> {
/// Constructor for creating a new RequestBuilder based on the Updater configuration and the
/// parameters for the current request.
pub fn new(config: &'a Config, params: &RequestParams) -> Self {
RequestBuilder {
config,
params: params.clone(),
app_entries: Vec::new(),
request_id: None,
session_id: None,
}
}
/// Insert the given app (with its cohort), and run the associated closure on it. If the app
/// already exists in the request (by app id), just run the closure on the AppEntry.
fn insert_and_modify_entry<F>(&mut self, app: &App, modify: F)
where
F: Fn(&mut AppEntry),
{
if let Some(app_entry) = self.app_entries.iter_mut().find(|e| e.app.id == app.id) {
// found an existing App in the Vec, so just run the closure on this AppEntry.
modify(app_entry);
} else {
// The App wasn't found, so add it to the list after running the closure on a newly
// generated AppEntry for this App.
let mut app_entry = AppEntry::new(app);
modify(&mut app_entry);
self.app_entries.push(app_entry);
}
}
/// This function adds an update check for the given App, in the given Cohort. This function is
/// an idempotent accumulator, in that it only once adds the App with it's associated Cohort to
/// the request. Afterward, it just marks the App as needing an update check.
pub fn add_update_check(mut self, app: &App) -> Self {
self.insert_and_modify_entry(app, |entry| {
entry.update_check = true;
});
self
}
/// This function adds a Ping for the given App, in the given Cohort. This function is an
/// idempotent accumulator, in that it only once adds the App with it's associated Cohort to the
/// request. Afterward, it just marks the App as needing a Ping.
pub fn add_ping(mut self, app: &App) -> Self {
self.insert_and_modify_entry(app, |entry| {
entry.ping = true;
});
self
}
/// This function adds an Event for the given App, in the given Cohort. This function is an
/// idempotent accumulator, in that it only once adds the App with it's associated Cohort to the
/// request. Afterward, it just adds the Event to the App.
pub fn add_event(mut self, app: &App, event: &Event) -> Self {
self.insert_and_modify_entry(app, |entry| {
entry.events.push(event.clone());
});
self
}
/// Set the request id of the request.
pub fn request_id(self, request_id: GUID) -> Self {
Self { request_id: Some(request_id), ..self }
}
/// Set the session id of the request.
pub fn session_id(self, session_id: GUID) -> Self {
Self { session_id: Some(session_id), ..self }
}
/// This function constructs the protocol::request::Request object from this Builder.
///
/// Note that the builder is not consumed in the process, and can be used afterward.
pub fn build(&self) -> Result<http::Request<hyper::Body>> {
let intermediate = self.build_intermediate();
info!("Building Request: {}", intermediate);
intermediate.into()
}
/// Helper function that constructs the request body from the builder.
fn build_intermediate(&self) -> Intermediate {
let mut headers = vec![
// Set the content-type to be JSON.
(http::header::CONTENT_TYPE.as_str(), "application/json".to_string()),
// The updater name header is always set directly from the name in the configuration
(HEADER_UPDATER_NAME, self.config.updater.name.clone()),
// The interactivity header is set based on the source of the request that's set in
// the request params
(
HEADER_INTERACTIVITY,
match self.params.source {
InstallSource::OnDemand => "fg".to_string(),
InstallSource::ScheduledTask => "bg".to_string(),
},
),
];
// And the app id header is based on the first app id in the request.
// TODO: Send all app ids, or only send the first based on configuration.
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(|entry| ProtocolApp::from(entry)).collect();
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.clone(),
is_machine: true,
request_id: self.request_id.clone(),
session_id: self.session_id.clone(),
os: self.config.os.clone(),
apps,
},
},
}
}
}
/// As the name implies, this is an intermediate that can be used to construct an http::Request from
/// the data that's in the Builder. It allows for type-aware inspection of the constructed protocol
/// request, as well as the full construction of the http request (uri, headers, body).
///
/// This struct owns all of it's data, so that they can be moved directly into the constructed http
/// request.
#[derive(Debug)]
struct Intermediate {
/// The URI for the http request.
uri: String,
/// The http request headers, in key:&str=value:String pairs
headers: Vec<(&'static str, String)>,
/// The request body, still in object form as a RequestWrapper
body: RequestWrapper,
}
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 body = serde_json::to_string(&intermediate.body)?;
let request = builder.body(body.into())?;
Ok(request)
}
}