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