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}