isolated_swd/
omaha.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// TODO(https://fxbug.dev/42128998): move everything except installer and policy into a shared crate,
6// because these modules all come from //third_party/rust_crates:omaha_client.
7mod installer;
8mod policy;
9use crate::omaha::installer::IsolatedInstaller;
10use anyhow::{Context, Error};
11use futures::lock::Mutex;
12use futures::prelude::*;
13use log::error;
14use omaha_client::app_set::VecAppSet;
15use omaha_client::common::App;
16use omaha_client::configuration::{Config, Updater};
17use omaha_client::cup_ecdsa::StandardCupv2Handler;
18use omaha_client::http_request::HttpRequest;
19use omaha_client::metrics::StubMetricsReporter;
20use omaha_client::protocol::Cohort;
21use omaha_client::protocol::request::OS;
22use omaha_client::state_machine::{
23    StateMachineBuilder, StateMachineEvent, UpdateCheckError, update_check,
24};
25use omaha_client::storage::MemStorage;
26use omaha_client::time::StandardTimeSource;
27use omaha_client::version::Version;
28use omaha_client_fuchsia::{http_request, timer};
29use std::rc::Rc;
30
31/// Get a |Config| object to use when making requests to Omaha.
32async fn get_omaha_config(version: &str, service_url: &str) -> Config {
33    Config {
34        updater: Updater { name: "Fuchsia".to_string(), version: Version::from([0, 0, 1, 0]) },
35
36        os: OS {
37            platform: "Fuchsia".to_string(),
38            version: version.to_string(),
39            service_pack: "".to_string(),
40            arch: std::env::consts::ARCH.to_string(),
41        },
42
43        service_url: service_url.to_owned(),
44        omaha_public_keys: None,
45    }
46}
47
48/// Get the update URL to use from Omaha, and install the update.
49pub async fn install_update(
50    updater: crate::updater::Updater,
51    appid: String,
52    service_url: String,
53    current_version: String,
54    channel: String,
55) -> Result<(), Error> {
56    let version = match current_version.parse::<Version>() {
57        Ok(version) => version,
58        Err(e) => {
59            error!("Unable to parse '{}' as Omaha version format: {:?}", current_version, e);
60            Version::from([0])
61        }
62    };
63
64    let cohort = Cohort { hint: Some(channel.clone()), name: Some(channel), ..Cohort::default() };
65    let app_set =
66        VecAppSet::new(vec![App::builder().id(appid).version(version).cohort(cohort).build()]);
67
68    let config = get_omaha_config(&current_version, &service_url).await;
69    install_update_with_http(updater, app_set, config, http_request::FuchsiaHyperHttpRequest::new())
70        .await
71        .context("installing update via http(s)")
72}
73
74#[derive(Debug, thiserror::Error)]
75enum InstallUpdateError {
76    #[error("expected exactly one UpdateCheckResult from Omaha, got {0:?}")]
77    WrongNumberOfUpdateCheckResults(Vec<Result<update_check::Response, UpdateCheckError>>),
78    #[error("expected exactly one app_response from Omaha, got {0:?}")]
79    WrongNumberOfAppResponses(Vec<omaha_client::state_machine::update_check::AppResponse>),
80    #[error("update check failed: {0}")]
81    UpdateCheckFailed(UpdateCheckError),
82    #[error("update check did not produce an update, took action {0:?}")]
83    DidNotUpdate(update_check::Action),
84}
85
86async fn install_update_with_http<HR>(
87    updater: crate::updater::Updater,
88    app_set: VecAppSet,
89    config: Config,
90    http_request: HR,
91) -> Result<(), InstallUpdateError>
92where
93    HR: HttpRequest,
94{
95    let storage = Rc::new(Mutex::new(MemStorage::new()));
96    let installer = IsolatedInstaller { updater };
97    let cup_handler = config.omaha_public_keys.as_ref().map(StandardCupv2Handler::new);
98    let state_machine = StateMachineBuilder::new(
99        policy::IsolatedPolicyEngine::new(StandardTimeSource),
100        http_request,
101        installer,
102        timer::FuchsiaTimer,
103        StubMetricsReporter,
104        storage,
105        config,
106        Rc::new(Mutex::new(app_set)),
107        cup_handler,
108    );
109
110    let stream: Vec<StateMachineEvent> = state_machine.oneshot_check().await.collect().await;
111
112    // TODO(https://fxbug.dev/42068623): expose this data via the Monitor protocol, not just a single event.
113    // Filter the state machine events down to just update check results,
114    // which contain the final state of the check.
115    let filtered_events: Vec<Result<update_check::Response, UpdateCheckError>> = stream
116        .into_iter()
117        .filter_map(|p| match p {
118            StateMachineEvent::UpdateCheckResult(val) => Some(val),
119            _ => None,
120        })
121        .collect();
122
123    // Ensure we only got one update check result
124    let [response]: [_; 1] =
125        filtered_events.try_into().map_err(InstallUpdateError::WrongNumberOfUpdateCheckResults)?;
126    let response = response.map_err(InstallUpdateError::UpdateCheckFailed)?;
127
128    // Ensure that update check only contained one app response
129    let [app_response]: [_; 1] =
130        response.app_responses.try_into().map_err(InstallUpdateError::WrongNumberOfAppResponses)?;
131
132    // Return the result of that single update check
133    match app_response.result {
134        update_check::Action::Updated => Ok(()),
135        other_action => Err(InstallUpdateError::DidNotUpdate(other_action)),
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::updater::for_tests::{UpdaterBuilder, UpdaterForTest, UpdaterResult};
143    use fuchsia_pkg_testing::PackageBuilder;
144
145    use mock_paver::{PaverEvent, hooks as mphooks};
146    use omaha_client::http_request::mock::MockHttpRequest;
147    use serde_json::json;
148
149    const TEST_REPO_URL: &str = "fuchsia-pkg://example.com";
150
151    const TEST_VERSION: &str = "20200101.0.0";
152    const TEST_CHANNEL: &str = "test-channel";
153    const TEST_APP_ID: &str = "qnzHyt4n";
154
155    /// Use the Omaha state machine to perform an update.
156    ///
157    /// Arguments:
158    /// * `updater`: UpdaterForTest environment to use with Omaha.
159    /// * `app_set`: AppSet for use by Omaha.
160    /// * `config`: Omaha client configuration.
161    /// * `mock_responses`: In-order list of responses Omaha should get for each HTTP request it
162    ///   makes.
163    async fn run_omaha(
164        updater: UpdaterForTest,
165        app_set: VecAppSet,
166        config: Config,
167        mock_responses: Vec<serde_json::Value>,
168    ) -> Result<UpdaterResult, Error> {
169        let mut http = MockHttpRequest::empty();
170
171        for response in mock_responses {
172            let response = serde_json::to_vec(&response).unwrap();
173            http.add_response(hyper::Response::new(response));
174        }
175
176        install_update_with_http(updater.updater, app_set, config, http)
177            .await
178            .context("Running omaha client")?;
179
180        Ok(UpdaterResult {
181            paver_events: updater.paver.take_events(),
182            expected_blobfs_contents: updater.expected_blobfs_contents,
183            resolver: updater.resolver,
184            realm_instance: updater.realm_instance,
185        })
186    }
187
188    fn get_test_app_set() -> VecAppSet {
189        VecAppSet::new(vec![
190            App::builder()
191                .id(TEST_APP_ID.to_owned())
192                .version([20200101, 0, 0, 0])
193                .cohort(Cohort::new(TEST_CHANNEL))
194                .build(),
195        ])
196    }
197
198    fn get_test_config() -> Config {
199        Config {
200            updater: Updater { name: "Fuchsia".to_owned(), version: Version::from([0, 0, 1, 0]) },
201            os: OS {
202                platform: "Fuchsia".to_owned(),
203                version: TEST_VERSION.to_owned(),
204                service_pack: "".to_owned(),
205                arch: std::env::consts::ARCH.to_owned(),
206            },
207
208            // Since we're using the mock http resolver, this doesn't matter.
209            service_url: "http://example.com".to_owned(),
210            omaha_public_keys: None,
211        }
212    }
213
214    fn get_test_response(package_path: &str) -> Vec<serde_json::Value> {
215        let update_response = json!({"response":{
216            "server": "prod",
217            "protocol": "3.0",
218            "app": [{
219                "appid": TEST_APP_ID,
220                "status": "ok",
221                "updatecheck": {
222                    "status": "ok",
223                    "urls": { "url": [{ "codebase": format!("{TEST_REPO_URL}/") }] },
224                    "manifest": {
225                        "version": "20200101.1.0.0",
226                        "actions": {
227                            "action": [
228                                {
229                                    "run": package_path,
230                                    "event": "install"
231                                },
232                                {
233                                    "event": "postinstall"
234                                }
235                            ]
236                        },
237                        "packages": {
238                            "package": [
239                                {
240                                    "name": package_path,
241                                    "fp": "2.20200101.1.0.0",
242                                    "required": true,
243                                }
244                            ]
245                        }
246                    }
247                }
248            }],
249        }});
250
251        let event_response = json!({"response":{
252            "server": "prod",
253            "protocol": "3.0",
254            "app": [{
255                "appid": TEST_APP_ID,
256                "status": "ok",
257            }]
258        }});
259
260        let response =
261            vec![update_response, event_response.clone(), event_response.clone(), event_response];
262        response
263    }
264
265    /// Construct an UpdaterForTest for use in the Omaha tests.
266    async fn build_updater() -> Result<UpdaterForTest, Error> {
267        let data = "hello world!".as_bytes();
268        let hook = |p: &PaverEvent| {
269            if let PaverEvent::QueryActiveConfiguration = p {
270                return zx::Status::NOT_SUPPORTED;
271            }
272            zx::Status::OK
273        };
274        let test_package = PackageBuilder::new("test_package")
275            .add_resource_at("bin/hello", "this is a test".as_bytes())
276            .add_resource_at("data/file", "this is a file".as_bytes())
277            .add_resource_at("meta/test_package.cm", "{}".as_bytes())
278            .build()
279            .await
280            .context("Building test_package")?;
281        let updater = UpdaterBuilder::new()
282            .await
283            .paver(|p| p.insert_hook(mphooks::return_error(hook)))
284            .repo_url(TEST_REPO_URL)
285            .add_package(test_package)
286            .fuchsia_image(data.to_vec(), Some(data.to_vec()))
287            .recovery_image(data.to_vec(), Some(data.to_vec()));
288        Ok(updater.build().await)
289    }
290
291    #[fuchsia::test]
292    pub async fn test_omaha_update() -> Result<(), Error> {
293        let updater = build_updater().await.context("Building updater")?;
294        let package_path = format!("update?hash={}", updater.update_merkle_root);
295        let app_set = get_test_app_set();
296        let config = get_test_config();
297        let response = get_test_response(&package_path);
298        let result =
299            run_omaha(updater, app_set, config, response).await.context("running omaha")?;
300
301        let () = result.verify_packages().await;
302        Ok(())
303    }
304
305    #[fuchsia::test]
306    pub async fn test_omaha_updater_reports_failure() -> Result<(), Error> {
307        let app_set = get_test_app_set();
308        let config = get_test_config();
309        let updater = build_updater().await.context("Building updater")?;
310
311        // No response, which means the update check should fail.
312        let response = vec![];
313
314        let result = run_omaha(updater, app_set, config, response).await;
315        assert!(result.is_err());
316        Ok(())
317    }
318
319    async fn build_updater_with_broken_paver() -> UpdaterForTest {
320        // Simulate the paver being completely broken, which means that installation should fail.
321        let hook = |_p: &PaverEvent| zx::Status::INTERNAL;
322
323        let updater = UpdaterBuilder::new()
324            .await
325            .paver(|p| p.insert_hook(mphooks::return_error(hook)))
326            .repo_url(TEST_REPO_URL)
327            .fuchsia_image(b"zbi-contents".to_vec(), None);
328
329        updater.build().await
330    }
331
332    #[fuchsia::test]
333    pub async fn test_installation_error_reports_failure() -> Result<(), Error> {
334        let app_set = get_test_app_set();
335        let config = get_test_config();
336        let updater = build_updater_with_broken_paver().await;
337        let package_path = format!("update?hash={}", updater.update_merkle_root);
338        let response = get_test_response(&package_path);
339
340        let result = run_omaha(updater, app_set, config, response).await;
341        assert!(result.is_err());
342        Ok(())
343    }
344}