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}