omaha_client/
request_builder.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
// Copyright 2019 The Fuchsia Authors
//
// Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
// This file may not be copied, modified, or distributed except according to
// those terms.

#[cfg(test)]
mod tests;

use crate::{
    common::{App, UserCounting},
    configuration::Config,
    cup_ecdsa::{CupDecorationError, CupRequest, Cupv2RequestHandler, RequestMetadata},
    protocol::{
        request::{
            Event, InstallSource, Ping, Request, RequestWrapper, UpdateCheck, GUID, HEADER_APP_ID,
            HEADER_INTERACTIVITY, HEADER_UPDATER_NAME,
        },
        PROTOCOL_V3,
    },
};
use http;
use std::fmt::Display;
use std::result;
use thiserror::Error;
use tracing::*;

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")]
    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),
}

/// 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,

    /// If true, the request should set the "updatedisabled" property for all apps in the update
    /// check request.
    pub disable_updates: bool,

    /// If true, the request should set the "sameversionupdate" property for all apps in the update
    /// check request.
    pub offer_update_if_same_version: 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,

    /// The updatecheck object if an update check should be performed, if None, the request will not
    /// include an updatecheck.
    update_check: Option<UpdateCheck>,

    /// 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: None,
            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.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,
        }
    }
}

/// 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: FnOnce(&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 adds the update check to the App.
    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
    }

    /// 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);
        });
        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,
        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,
        ))
    }

    /// Helper function that constructs the request body from the builder.
    fn build_intermediate(
        &self,
        cup_handler: Option<&impl Cupv2RequestHandler>,
    ) -> Result<(Intermediate, Option<RequestMetadata>)> {
        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(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))
    }
}

/// 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)]
pub struct Intermediate {
    /// The URI for the http request.
    pub uri: String,

    /// The http request headers, in key:&str=value:String pairs
    pub headers: Vec<(&'static str, String)>,

    /// The request body, still in object form as a RequestWrapper
    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()
    }
}