Skip to main content

omaha_client/
request_builder.rs

1// Copyright 2019 The Fuchsia Authors
2//
3// Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
4// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
5// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
6// This file may not be copied, modified, or distributed except according to
7// those terms.
8
9#[cfg(test)]
10mod tests;
11
12use crate::{
13    common::{App, UserCounting},
14    configuration::Config,
15    cup_ecdsa::{CupDecorationError, CupRequest, Cupv2RequestHandler, RequestMetadata},
16    http_request::Body,
17    protocol::{
18        PROTOCOL_V3,
19        request::{
20            Event, GUID, HEADER_APP_ID, HEADER_INTERACTIVITY, HEADER_UPDATER_NAME, InstallSource,
21            Ping, Request, RequestWrapper, UpdateCheck,
22        },
23    },
24};
25use http;
26use log::{info, warn};
27use std::fmt::Display;
28use std::result;
29use thiserror::Error;
30
31type ProtocolApp = crate::protocol::request::App;
32
33/// Building a request can fail for multiple reasons, this enum consolidates them into a single
34/// type that can be used to express those reasons.
35#[derive(Debug, Error)]
36pub enum Error {
37    #[error("Unexpected JSON error constructing update check")]
38    Json(#[from] serde_json::Error),
39
40    #[error("Http error performing update check")]
41    Http(#[from] http::Error),
42
43    #[error("Error decorating outgoing request with CUPv2 parameters")]
44    Cup(#[from] CupDecorationError),
45}
46
47/// The builder's own Result type.
48pub type Result<T> = result::Result<T, Error>;
49
50/// These are the parameters that describe how the request should be performed.
51#[derive(Clone, Debug, Default, Eq, PartialEq)]
52pub struct RequestParams {
53    /// The install source for a request changes a number of properties of the request, including
54    /// the HTTP request headers, and influences how Omaha services the request (e.g. throttling)
55    pub source: InstallSource,
56
57    /// If true, the request should use any configured proxies.  This allows the bypassing of
58    /// proxies if there are difficulties in communicating with the Omaha service.
59    pub use_configured_proxies: bool,
60
61    /// If true, the request should set the "updatedisabled" property for all apps in the update
62    /// check request.
63    pub disable_updates: bool,
64
65    /// If true, the request should set the "sameversionupdate" property for all apps in the update
66    /// check request.
67    pub offer_update_if_same_version: bool,
68}
69
70/// The AppEntry holds the data for the app whose request is currently being constructed.  An app
71/// can only have a single cohort, update check, or ping, but may have multiple events.  Note that
72/// while this object allows for no update check, no ping, and no events, that doesn't make sense
73/// via the protocol.
74///
75/// This struct has ownership over it's members, so that they may later be moved out when the
76/// request itself is built.
77#[derive(Clone)]
78struct AppEntry {
79    /// The identifying data for the application.
80    app: App,
81
82    /// The updatecheck object if an update check should be performed, if None, the request will not
83    /// include an updatecheck.
84    update_check: Option<UpdateCheck>,
85
86    /// Set to true if a ping should be send.
87    ping: bool,
88
89    /// Any events that need to be sent to the Omaha service.
90    events: Vec<Event>,
91}
92
93impl AppEntry {
94    /// Basic constructor for the AppEntry.  All AppEntries MUST have an App and a Cohort,
95    /// everything else can be omitted.
96    fn new(app: &App) -> AppEntry {
97        AppEntry { app: app.clone(), update_check: None, ping: false, events: Vec::new() }
98    }
99}
100
101/// Conversion method to construct a ProtocolApp from an AppEntry.  This consumes the entry, moving
102/// it's members into the generated ProtocolApp.
103impl From<AppEntry> for ProtocolApp {
104    fn from(entry: AppEntry) -> ProtocolApp {
105        if entry.update_check.is_none() && entry.events.is_empty() && !entry.ping {
106            warn!(
107                "Generated protocol::request for {} has no update check, ping, or events",
108                entry.app.id
109            );
110        }
111        let ping = if entry.ping {
112            let UserCounting::ClientRegulatedByDate(days) = entry.app.user_counting;
113            Some(Ping { date_last_active: days, date_last_roll_call: days })
114        } else {
115            None
116        };
117        ProtocolApp {
118            id: entry.app.id,
119            version: entry.app.version.to_string(),
120            fingerprint: entry.app.fingerprint,
121            cohort: Some(entry.app.cohort),
122            update_check: entry.update_check,
123            events: entry.events,
124            ping,
125            extra_fields: entry.app.extra_fields,
126        }
127    }
128}
129
130/// The RequestBuilder is used to create the protocol requests.  Each request is represented by an
131/// instance of protocol::request::Request.
132pub struct RequestBuilder<'a> {
133    // The static data identifying the updater binary.
134    config: &'a Config,
135
136    // The parameters that control how this request is to be made.
137    params: RequestParams,
138
139    // The applications to include in this request, with their associated update checks, pings, and
140    // events to report.
141    app_entries: Vec<AppEntry>,
142
143    request_id: Option<GUID>,
144    session_id: Option<GUID>,
145}
146
147/// The RequestBuilder is a stateful builder for protocol::request::Request objects.  After being
148/// instantiated with the base parameters for the current request, it has functions for accumulating
149/// an update check, a ping, and multiple events for individual App objects.
150///
151/// The 'add_*()' functions are all insensitive to order for a given App and it's Cohort.  However,
152/// if multiple different App entries are used, then order matters.  The order in the request is
153/// the order that the Apps are added to the RequestBuilder.
154///
155/// Further, the cohort is only captured on the _first_ time a given App is added to the request.
156/// If, for some reason, the same App is added twice, but with a different cohort, the latter cohort
157/// is ignored.
158///
159/// The operation being added (update check, ping, or event) is added to the existing App.  The app
160/// maintains its existing place in the list of Apps to be added to the request.
161impl<'a> RequestBuilder<'a> {
162    /// Constructor for creating a new RequestBuilder based on the Updater configuration and the
163    /// parameters for the current request.
164    pub fn new(config: &'a Config, params: &RequestParams) -> Self {
165        RequestBuilder {
166            config,
167            params: params.clone(),
168            app_entries: Vec::new(),
169            request_id: None,
170            session_id: None,
171        }
172    }
173
174    /// Insert the given app (with its cohort), and run the associated closure on it.  If the app
175    /// already exists in the request (by app id), just run the closure on the AppEntry.
176    fn insert_and_modify_entry<F>(&mut self, app: &App, modify: F)
177    where
178        F: FnOnce(&mut AppEntry),
179    {
180        if let Some(app_entry) = self.app_entries.iter_mut().find(|e| e.app.id == app.id) {
181            // found an existing App in the Vec, so just run the closure on this AppEntry.
182            modify(app_entry);
183        } else {
184            // The App wasn't found, so add it to the list after running the closure on a newly
185            // generated AppEntry for this App.
186            let mut app_entry = AppEntry::new(app);
187            modify(&mut app_entry);
188            self.app_entries.push(app_entry);
189        }
190    }
191
192    /// This function adds an update check for the given App, in the given Cohort.  This function is
193    /// an idempotent accumulator, in that it only once adds the App with it's associated Cohort to
194    /// the request.  Afterward, it just adds the update check to the App.
195    pub fn add_update_check(mut self, app: &App) -> Self {
196        let update_check = UpdateCheck {
197            disabled: self.params.disable_updates,
198            offer_update_if_same_version: self.params.offer_update_if_same_version,
199        };
200
201        self.insert_and_modify_entry(app, |entry| {
202            entry.update_check = Some(update_check);
203        });
204        self
205    }
206
207    /// This function adds a Ping for the given App, in the given Cohort.  This function is an
208    /// idempotent accumulator, in that it only once adds the App with it's associated Cohort to the
209    /// request.  Afterward, it just marks the App as needing a Ping.
210    pub fn add_ping(mut self, app: &App) -> Self {
211        self.insert_and_modify_entry(app, |entry| {
212            entry.ping = true;
213        });
214        self
215    }
216
217    /// This function adds an Event for the given App, in the given Cohort.  This function is an
218    /// idempotent accumulator, in that it only once adds the App with it's associated Cohort to the
219    /// request.  Afterward, it just adds the Event to the App.
220    pub fn add_event(mut self, app: &App, event: Event) -> Self {
221        self.insert_and_modify_entry(app, |entry| {
222            entry.events.push(event);
223        });
224        self
225    }
226
227    /// Set the request id of the request.
228    pub fn request_id(self, request_id: GUID) -> Self {
229        Self { request_id: Some(request_id), ..self }
230    }
231
232    /// Set the session id of the request.
233    pub fn session_id(self, session_id: GUID) -> Self {
234        Self { session_id: Some(session_id), ..self }
235    }
236
237    /// This function constructs the protocol::request::Request object from this Builder.
238    ///
239    /// Note that the builder is not consumed in the process, and can be used afterward.
240    pub fn build(
241        &self,
242        cup_handler: Option<&impl Cupv2RequestHandler>,
243    ) -> Result<(http::Request<Body>, Option<RequestMetadata>)> {
244        let (intermediate, request_metadata) = self.build_intermediate(cup_handler)?;
245        if self.app_entries.iter().any(|app| app.update_check.is_some()) {
246            info!("Building Request: {}", intermediate);
247        }
248        Ok((Into::<Result<http::Request<Body>>>::into(intermediate)?, request_metadata))
249    }
250
251    /// Helper function that constructs the request body from the builder.
252    fn build_intermediate(
253        &self,
254        cup_handler: Option<&impl Cupv2RequestHandler>,
255    ) -> Result<(Intermediate, Option<RequestMetadata>)> {
256        let mut headers = vec![
257            // Set the content-type to be JSON.
258            (http::header::CONTENT_TYPE.as_str(), "application/json".to_string()),
259            // The updater name header is always set directly from the name in the configuration
260            (HEADER_UPDATER_NAME, self.config.updater.name.clone()),
261            // The interactivity header is set based on the source of the request that's set in
262            // the request params
263            (
264                HEADER_INTERACTIVITY,
265                match self.params.source {
266                    InstallSource::OnDemand => "fg".to_string(),
267                    InstallSource::ScheduledTask => "bg".to_string(),
268                },
269            ),
270        ];
271        // And the app id header is based on the first app id in the request.
272        // TODO: Send all app ids, or only send the first based on configuration.
273        if let Some(main_app) = self.app_entries.first() {
274            headers.push((HEADER_APP_ID, main_app.app.id.clone()));
275        }
276
277        let apps = self.app_entries.iter().cloned().map(ProtocolApp::from).collect();
278
279        let mut intermediate = Intermediate {
280            uri: self.config.service_url.clone(),
281            headers,
282            body: RequestWrapper {
283                request: Request {
284                    protocol_version: PROTOCOL_V3.to_string(),
285                    updater: self.config.updater.name.clone(),
286                    updater_version: self.config.updater.version.to_string(),
287                    install_source: self.params.source,
288                    is_machine: true,
289                    request_id: self.request_id.clone(),
290                    session_id: self.session_id.clone(),
291                    os: self.config.os.clone(),
292                    apps,
293                },
294            },
295        };
296
297        let request_metadata = match cup_handler.as_ref() {
298            Some(handler) => Some(handler.decorate_request(&mut intermediate)?),
299            _ => None,
300        };
301
302        Ok((intermediate, request_metadata))
303    }
304}
305
306/// Intermediate constructs an http::Request from available data.
307///
308/// As the name implies, this is an intermediate that can be used to construct an http::Request from
309/// the data that's in the Builder.  It allows for type-aware inspection of the constructed protocol
310/// request, as well as the full construction of the http request (uri, headers, body).
311///
312/// This struct owns all of it's data, so that they can be moved directly into the constructed http
313/// request.
314#[derive(Debug)]
315pub struct Intermediate {
316    /// The URI for the http request.
317    pub uri: String,
318
319    /// The http request headers, in key:&str=value:String pairs
320    pub headers: Vec<(&'static str, String)>,
321
322    /// The request body, still in object form as a RequestWrapper
323    pub body: RequestWrapper,
324}
325
326impl Intermediate {
327    pub fn serialize_body(&self) -> serde_json::Result<Vec<u8>> {
328        serde_json::to_vec(&self.body)
329    }
330}
331
332impl Display for Intermediate {
333    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334        writeln!(f, "uri: {} ", self.uri)?;
335        for (name, value) in &self.headers {
336            writeln!(f, "header: {name}={value}")?;
337        }
338        match serde_json::to_value(&self.body) {
339            Ok(value) => writeln!(f, "body: {value:#}"),
340            Err(e) => writeln!(f, "err: {e}"),
341        }
342    }
343}
344
345impl From<Intermediate> for Result<http::Request<Body>> {
346    fn from(intermediate: Intermediate) -> Self {
347        let mut builder = hyper::Request::post(&intermediate.uri);
348        for (key, value) in &intermediate.headers {
349            builder = builder.header(*key, value);
350        }
351
352        let request =
353            builder.body(Body::from(bytes::Bytes::from(intermediate.serialize_body()?)))?;
354        Ok(request)
355    }
356}
357
358impl CupRequest for Intermediate {
359    fn get_uri(&self) -> &str {
360        &self.uri
361    }
362    fn set_uri(&mut self, uri: String) {
363        self.uri = uri;
364    }
365    fn get_serialized_body(&self) -> serde_json::Result<Vec<u8>> {
366        self.serialize_body()
367    }
368}