update_engine: Move Omaha xml request generation into its own file
omaha_request_action.cc is becoming large and unmanagable. This CL moves
the code related to the XML request building process into its own file
so it can be managed properly. In the future we can clean it up and use
more proper XML builders like tinyxml2.
There is no semantic change in this. It just moves that part of the code
into another file.
BUG=none
TEST=unittest
Change-Id: If774d86f6b29dd17963bec94bb6e91e2f4109a12
Reviewed-on: https://chromium-review.googlesource.com/1544892
Commit-Ready: ChromeOS CL Exonerator Bot <chromiumos-cl-exonerator@appspot.gserviceaccount.com>
Tested-by: Amin Hassani <ahassani@chromium.org>
Reviewed-by: Sen Jiang <senj@chromium.org>
diff --git a/common/constants.cc b/common/constants.cc
index 310f1b2..5ab96b0 100644
--- a/common/constants.cc
+++ b/common/constants.cc
@@ -124,4 +124,6 @@
// The default is 1 (always run post install).
const char kPayloadPropertyRunPostInstall[] = "RUN_POST_INSTALL";
+const char kOmahaUpdaterVersion[] = "0.1.0.0";
+
} // namespace chromeos_update_engine
diff --git a/common/constants.h b/common/constants.h
index d5a8ae3..9b4623f 100644
--- a/common/constants.h
+++ b/common/constants.h
@@ -108,6 +108,8 @@
extern const char kPayloadPropertySwitchSlotOnReboot[];
extern const char kPayloadPropertyRunPostInstall[];
+extern const char kOmahaUpdaterVersion[];
+
// A download source is any combination of protocol and server (that's of
// interest to us when looking at UMA metrics) using which we may download
// the payload.
diff --git a/omaha_request_action.cc b/omaha_request_action.cc
index deb294a..5b69ec8 100644
--- a/omaha_request_action.cc
+++ b/omaha_request_action.cc
@@ -48,6 +48,7 @@
#include "update_engine/connection_manager_interface.h"
#include "update_engine/metrics_reporter_interface.h"
#include "update_engine/metrics_utils.h"
+#include "update_engine/omaha_request_builder_xml.h"
#include "update_engine/omaha_request_params.h"
#include "update_engine/p2p_manager.h"
#include "update_engine/payload_state_interface.h"
@@ -106,8 +107,6 @@
constexpr char kValPostInstall[] = "postinstall";
constexpr char kValNoUpdate[] = "noupdate";
-constexpr char kOmahaUpdaterVersion[] = "0.1.0.0";
-
// X-Goog-Update headers.
constexpr char kXGoogleUpdateInteractivity[] = "X-Goog-Update-Interactivity";
constexpr char kXGoogleUpdateAppId[] = "X-Goog-Update-AppId";
@@ -119,362 +118,6 @@
constexpr char kAttrFirmwareVersion[] = "firmware_version";
constexpr char kAttrKernelVersion[] = "kernel_version";
-namespace {
-
-// Returns an XML ping element attribute assignment with attribute
-// |name| and value |ping_days| if |ping_days| has a value that needs
-// to be sent, or an empty string otherwise.
-string GetPingAttribute(const string& name, int ping_days) {
- if (ping_days > 0 || ping_days == OmahaRequestAction::kNeverPinged)
- return base::StringPrintf(" %s=\"%d\"", name.c_str(), ping_days);
- return "";
-}
-
-// Returns an XML ping element if any of the elapsed days need to be
-// sent, or an empty string otherwise.
-string GetPingXml(int ping_active_days, int ping_roll_call_days) {
- string ping_active = GetPingAttribute("a", ping_active_days);
- string ping_roll_call = GetPingAttribute("r", ping_roll_call_days);
- if (!ping_active.empty() || !ping_roll_call.empty()) {
- return base::StringPrintf(" <ping active=\"1\"%s%s></ping>\n",
- ping_active.c_str(),
- ping_roll_call.c_str());
- }
- return "";
-}
-
-// Returns an XML that goes into the body of the <app> element of the Omaha
-// request based on the given parameters.
-string GetAppBody(const OmahaEvent* event,
- OmahaRequestParams* params,
- bool ping_only,
- bool include_ping,
- bool skip_updatecheck,
- int ping_active_days,
- int ping_roll_call_days,
- PrefsInterface* prefs) {
- string app_body;
- if (event == nullptr) {
- if (include_ping)
- app_body = GetPingXml(ping_active_days, ping_roll_call_days);
- if (!ping_only) {
- if (!skip_updatecheck) {
- app_body += " <updatecheck";
- if (!params->target_version_prefix().empty()) {
- app_body += base::StringPrintf(
- " targetversionprefix=\"%s\"",
- XmlEncodeWithDefault(params->target_version_prefix(), "")
- .c_str());
- // Rollback requires target_version_prefix set.
- if (params->rollback_allowed()) {
- app_body += " rollback_allowed=\"true\"";
- }
- }
- app_body += "></updatecheck>\n";
- }
-
- // If this is the first update check after a reboot following a previous
- // update, generate an event containing the previous version number. If
- // the previous version preference file doesn't exist the event is still
- // generated with a previous version of 0.0.0.0 -- this is relevant for
- // older clients or new installs. The previous version event is not sent
- // for ping-only requests because they come before the client has
- // rebooted. The previous version event is also not sent if it was already
- // sent for this new version with a previous updatecheck.
- string prev_version;
- if (!prefs->GetString(kPrefsPreviousVersion, &prev_version)) {
- prev_version = "0.0.0.0";
- }
- // We only store a non-empty previous version value after a successful
- // update in the previous boot. After reporting it back to the server,
- // we clear the previous version value so it doesn't get reported again.
- if (!prev_version.empty()) {
- app_body += base::StringPrintf(
- " <event eventtype=\"%d\" eventresult=\"%d\" "
- "previousversion=\"%s\"></event>\n",
- OmahaEvent::kTypeRebootedAfterUpdate,
- OmahaEvent::kResultSuccess,
- XmlEncodeWithDefault(prev_version, "0.0.0.0").c_str());
- LOG_IF(WARNING, !prefs->SetString(kPrefsPreviousVersion, ""))
- << "Unable to reset the previous version.";
- }
- }
- } else {
- // The error code is an optional attribute so append it only if the result
- // is not success.
- string error_code;
- if (event->result != OmahaEvent::kResultSuccess) {
- error_code = base::StringPrintf(" errorcode=\"%d\"",
- static_cast<int>(event->error_code));
- }
- app_body = base::StringPrintf(
- " <event eventtype=\"%d\" eventresult=\"%d\"%s></event>\n",
- event->type,
- event->result,
- error_code.c_str());
- }
-
- return app_body;
-}
-
-// Returns the cohort* argument to include in the <app> tag for the passed
-// |arg_name| and |prefs_key|, if any. The return value is suitable to
-// concatenate to the list of arguments and includes a space at the end.
-string GetCohortArgXml(PrefsInterface* prefs,
- const string arg_name,
- const string prefs_key) {
- // There's nothing wrong with not having a given cohort setting, so we check
- // existence first to avoid the warning log message.
- if (!prefs->Exists(prefs_key))
- return "";
- string cohort_value;
- if (!prefs->GetString(prefs_key, &cohort_value) || cohort_value.empty())
- return "";
- // This is a sanity check to avoid sending a huge XML file back to Ohama due
- // to a compromised stateful partition making the update check fail in low
- // network environments envent after a reboot.
- if (cohort_value.size() > 1024) {
- LOG(WARNING) << "The omaha cohort setting " << arg_name
- << " has a too big value, which must be an error or an "
- "attacker trying to inhibit updates.";
- return "";
- }
-
- string escaped_xml_value;
- if (!XmlEncode(cohort_value, &escaped_xml_value)) {
- LOG(WARNING) << "The omaha cohort setting " << arg_name
- << " is ASCII-7 invalid, ignoring it.";
- return "";
- }
-
- return base::StringPrintf(
- "%s=\"%s\" ", arg_name.c_str(), escaped_xml_value.c_str());
-}
-
-struct OmahaAppData {
- string id;
- string version;
- string product_components;
-};
-
-bool IsValidComponentID(const string& id) {
- for (char c : id) {
- if (!isalnum(c) && c != '-' && c != '_' && c != '.')
- return false;
- }
- return true;
-}
-
-// Returns an XML that corresponds to the entire <app> node of the Omaha
-// request based on the given parameters.
-string GetAppXml(const OmahaEvent* event,
- OmahaRequestParams* params,
- const OmahaAppData& app_data,
- bool ping_only,
- bool include_ping,
- bool skip_updatecheck,
- int ping_active_days,
- int ping_roll_call_days,
- int install_date_in_days,
- SystemState* system_state) {
- string app_body = GetAppBody(event,
- params,
- ping_only,
- include_ping,
- skip_updatecheck,
- ping_active_days,
- ping_roll_call_days,
- system_state->prefs());
- string app_versions;
-
- // If we are downgrading to a more stable channel and we are allowed to do
- // powerwash, then pass 0.0.0.0 as the version. This is needed to get the
- // highest-versioned payload on the destination channel.
- if (params->ShouldPowerwash()) {
- LOG(INFO) << "Passing OS version as 0.0.0.0 as we are set to powerwash "
- << "on downgrading to the version in the more stable channel";
- app_versions = "version=\"0.0.0.0\" from_version=\"" +
- XmlEncodeWithDefault(app_data.version, "0.0.0.0") + "\" ";
- } else {
- app_versions = "version=\"" +
- XmlEncodeWithDefault(app_data.version, "0.0.0.0") + "\" ";
- }
-
- string download_channel = params->download_channel();
- string app_channels =
- "track=\"" + XmlEncodeWithDefault(download_channel, "") + "\" ";
- if (params->current_channel() != download_channel) {
- app_channels += "from_track=\"" +
- XmlEncodeWithDefault(params->current_channel(), "") + "\" ";
- }
-
- string delta_okay_str = params->delta_okay() ? "true" : "false";
-
- // If install_date_days is not set (e.g. its value is -1 ), don't
- // include the attribute.
- string install_date_in_days_str = "";
- if (install_date_in_days >= 0) {
- install_date_in_days_str =
- base::StringPrintf("installdate=\"%d\" ", install_date_in_days);
- }
-
- string app_cohort_args;
- app_cohort_args +=
- GetCohortArgXml(system_state->prefs(), "cohort", kPrefsOmahaCohort);
- app_cohort_args += GetCohortArgXml(
- system_state->prefs(), "cohorthint", kPrefsOmahaCohortHint);
- app_cohort_args += GetCohortArgXml(
- system_state->prefs(), "cohortname", kPrefsOmahaCohortName);
-
- string fingerprint_arg;
- if (!params->os_build_fingerprint().empty()) {
- fingerprint_arg = "fingerprint=\"" +
- XmlEncodeWithDefault(params->os_build_fingerprint(), "") +
- "\" ";
- }
-
- string buildtype_arg;
- if (!params->os_build_type().empty()) {
- buildtype_arg = "os_build_type=\"" +
- XmlEncodeWithDefault(params->os_build_type(), "") + "\" ";
- }
-
- string product_components_args;
- if (!params->ShouldPowerwash() && !app_data.product_components.empty()) {
- brillo::KeyValueStore store;
- if (store.LoadFromString(app_data.product_components)) {
- for (const string& key : store.GetKeys()) {
- if (!IsValidComponentID(key)) {
- LOG(ERROR) << "Invalid component id: " << key;
- continue;
- }
- string version;
- if (!store.GetString(key, &version)) {
- LOG(ERROR) << "Failed to get version for " << key
- << " in product_components.";
- continue;
- }
- product_components_args +=
- base::StringPrintf("_%s.version=\"%s\" ",
- key.c_str(),
- XmlEncodeWithDefault(version, "").c_str());
- }
- } else {
- LOG(ERROR) << "Failed to parse product_components:\n"
- << app_data.product_components;
- }
- }
-
- // clang-format off
- string app_xml = " <app "
- "appid=\"" + XmlEncodeWithDefault(app_data.id, "") + "\" " +
- app_cohort_args +
- app_versions +
- app_channels +
- product_components_args +
- fingerprint_arg +
- buildtype_arg +
- "lang=\"" + XmlEncodeWithDefault(params->app_lang(), "en-US") + "\" " +
- "board=\"" + XmlEncodeWithDefault(params->os_board(), "") + "\" " +
- "hardware_class=\"" + XmlEncodeWithDefault(params->hwid(), "") + "\" " +
- "delta_okay=\"" + delta_okay_str + "\" "
- "fw_version=\"" + XmlEncodeWithDefault(params->fw_version(), "") + "\" " +
- "ec_version=\"" + XmlEncodeWithDefault(params->ec_version(), "") + "\" " +
- install_date_in_days_str +
- ">\n" +
- app_body +
- " </app>\n";
- // clang-format on
- return app_xml;
-}
-
-// Returns an XML that corresponds to the entire <os> node of the Omaha
-// request based on the given parameters.
-string GetOsXml(OmahaRequestParams* params) {
- string os_xml =
- " <os "
- "version=\"" +
- XmlEncodeWithDefault(params->os_version(), "") + "\" " + "platform=\"" +
- XmlEncodeWithDefault(params->os_platform(), "") + "\" " + "sp=\"" +
- XmlEncodeWithDefault(params->os_sp(), "") +
- "\">"
- "</os>\n";
- return os_xml;
-}
-
-// Returns an XML that corresponds to the entire Omaha request based on the
-// given parameters.
-string GetRequestXml(const OmahaEvent* event,
- OmahaRequestParams* params,
- bool ping_only,
- bool include_ping,
- int ping_active_days,
- int ping_roll_call_days,
- int install_date_in_days,
- SystemState* system_state) {
- string os_xml = GetOsXml(params);
- OmahaAppData product_app = {
- .id = params->GetAppId(),
- .version = params->app_version(),
- .product_components = params->product_components()};
- // Skips updatecheck for platform app in case of an install operation.
- string app_xml = GetAppXml(event,
- params,
- product_app,
- ping_only,
- include_ping,
- params->is_install(), /* skip_updatecheck */
- ping_active_days,
- ping_roll_call_days,
- install_date_in_days,
- system_state);
- if (!params->system_app_id().empty()) {
- OmahaAppData system_app = {.id = params->system_app_id(),
- .version = params->system_version()};
- app_xml += GetAppXml(event,
- params,
- system_app,
- ping_only,
- include_ping,
- false, /* skip_updatecheck */
- ping_active_days,
- ping_roll_call_days,
- install_date_in_days,
- system_state);
- }
- // Create APP ID according to |dlc_module_id| (sticking the current AppID to
- // the DLC module ID with an underscode).
- for (const auto& dlc_module_id : params->dlc_module_ids()) {
- OmahaAppData dlc_module_app = {
- .id = params->GetAppId() + "_" + dlc_module_id,
- .version = params->app_version()};
- app_xml += GetAppXml(event,
- params,
- dlc_module_app,
- ping_only,
- include_ping,
- false, /* skip_updatecheck */
- ping_active_days,
- ping_roll_call_days,
- install_date_in_days,
- system_state);
- }
-
- string request_xml = base::StringPrintf(
- "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
- "<request protocol=\"3.0\" updater=\"%s\" updaterversion=\"%s\""
- " installsource=\"%s\" ismachine=\"1\">\n%s%s</request>\n",
- constants::kOmahaUpdaterID,
- kOmahaUpdaterVersion,
- params->interactive() ? "ondemandupdate" : "scheduler",
- os_xml.c_str(),
- app_xml.c_str());
-
- return request_xml;
-}
-
-} // namespace
-
// Struct used for holding data obtained when parsing the XML.
struct OmahaParserData {
explicit OmahaParserData(XML_Parser _xml_parser) : xml_parser(_xml_parser) {}
@@ -638,49 +281,6 @@
} // namespace
-bool XmlEncode(const string& input, string* output) {
- if (std::find_if(input.begin(), input.end(), [](const char c) {
- return c & 0x80;
- }) != input.end()) {
- LOG(WARNING) << "Invalid ASCII-7 string passed to the XML encoder:";
- utils::HexDumpString(input);
- return false;
- }
- output->clear();
- // We need at least input.size() space in the output, but the code below will
- // handle it if we need more.
- output->reserve(input.size());
- for (char c : input) {
- switch (c) {
- case '\"':
- output->append(""");
- break;
- case '\'':
- output->append("'");
- break;
- case '&':
- output->append("&");
- break;
- case '<':
- output->append("<");
- break;
- case '>':
- output->append(">");
- break;
- default:
- output->push_back(c);
- }
- }
- return true;
-}
-
-string XmlEncodeWithDefault(const string& input, const string& default_value) {
- string output;
- if (XmlEncode(input, &output))
- return output;
- return default_value;
-}
-
OmahaRequestAction::OmahaRequestAction(
SystemState* system_state,
OmahaEvent* event,
@@ -732,8 +332,8 @@
}
bool OmahaRequestAction::ShouldPing() const {
- if (ping_active_days_ == OmahaRequestAction::kNeverPinged &&
- ping_roll_call_days_ == OmahaRequestAction::kNeverPinged) {
+ if (ping_active_days_ == kNeverPinged &&
+ ping_roll_call_days_ == kNeverPinged) {
int powerwash_count = system_state_->hardware()->GetPowerwashCount();
if (powerwash_count > 0) {
LOG(INFO) << "Not sending ping with a=-1 r=-1 to omaha because "
diff --git a/omaha_request_action.h b/omaha_request_action.h
index 8db5fb9..8e81af9 100644
--- a/omaha_request_action.h
+++ b/omaha_request_action.h
@@ -33,6 +33,7 @@
#include "update_engine/common/action.h"
#include "update_engine/common/http_fetcher.h"
+#include "update_engine/omaha_request_builder_xml.h"
#include "update_engine/omaha_response.h"
#include "update_engine/system_state.h"
@@ -45,56 +46,6 @@
namespace chromeos_update_engine {
-// Encodes XML entities in a given string. Input must be ASCII-7 valid. If
-// the input is invalid, the default value is used instead.
-std::string XmlEncodeWithDefault(const std::string& input,
- const std::string& default_value);
-
-// Escapes text so it can be included as character data and attribute
-// values. The |input| string must be valid ASCII-7, no UTF-8 supported.
-// Returns whether the |input| was valid and escaped properly in |output|.
-bool XmlEncode(const std::string& input, std::string* output);
-
-// This struct encapsulates the Omaha event information. For a
-// complete list of defined event types and results, see
-// http://code.google.com/p/omaha/wiki/ServerProtocol#event
-struct OmahaEvent {
- // The Type values correspond to EVENT_TYPE values of Omaha.
- enum Type {
- kTypeUnknown = 0,
- kTypeDownloadComplete = 1,
- kTypeInstallComplete = 2,
- kTypeUpdateComplete = 3,
- kTypeUpdateDownloadStarted = 13,
- kTypeUpdateDownloadFinished = 14,
- // Chromium OS reserved type sent after the first reboot following an update
- // completed.
- kTypeRebootedAfterUpdate = 54,
- };
-
- // The Result values correspond to EVENT_RESULT values of Omaha.
- enum Result {
- kResultError = 0,
- kResultSuccess = 1,
- kResultUpdateDeferred = 9, // When we ignore/defer updates due to policy.
- };
-
- OmahaEvent()
- : type(kTypeUnknown),
- result(kResultError),
- error_code(ErrorCode::kError) {}
- explicit OmahaEvent(Type in_type)
- : type(in_type),
- result(kResultSuccess),
- error_code(ErrorCode::kSuccess) {}
- OmahaEvent(Type in_type, Result in_result, ErrorCode in_error_code)
- : type(in_type), result(in_result), error_code(in_error_code) {}
-
- Type type;
- Result result;
- ErrorCode error_code;
-};
-
class NoneType;
class OmahaRequestAction;
class OmahaRequestParams;
@@ -116,7 +67,6 @@
class OmahaRequestAction : public Action<OmahaRequestAction>,
public HttpFetcherDelegate {
public:
- static const int kNeverPinged = -1;
static const int kPingTimeJump = -2;
// We choose this value of 10 as a heuristic for a work day in trying
// each URL, assuming we check roughly every 45 mins. This is a good time to
diff --git a/omaha_request_action_unittest.cc b/omaha_request_action_unittest.cc
index a642b5a..e7c610f 100644
--- a/omaha_request_action_unittest.cc
+++ b/omaha_request_action_unittest.cc
@@ -51,6 +51,7 @@
#include "update_engine/metrics_reporter_interface.h"
#include "update_engine/mock_connection_manager.h"
#include "update_engine/mock_payload_state.h"
+#include "update_engine/omaha_request_builder_xml.h"
#include "update_engine/omaha_request_params.h"
#include "update_engine/update_manager/rollback_prefs.h"
@@ -1755,27 +1756,6 @@
EXPECT_FALSE(loop.PendingTasks());
}
-TEST_F(OmahaRequestActionTest, XmlEncodeTest) {
- string output;
- EXPECT_TRUE(XmlEncode("ab", &output));
- EXPECT_EQ("ab", output);
- EXPECT_TRUE(XmlEncode("a<b", &output));
- EXPECT_EQ("a<b", output);
- EXPECT_TRUE(XmlEncode("<&>\"\'\\", &output));
- EXPECT_EQ("<&>"'\\", output);
- EXPECT_TRUE(XmlEncode("<&>", &output));
- EXPECT_EQ("&lt;&amp;&gt;", output);
- // Check that unterminated UTF-8 strings are handled properly.
- EXPECT_FALSE(XmlEncode("\xc2", &output));
- // Fail with invalid ASCII-7 chars.
- EXPECT_FALSE(XmlEncode("This is an 'n' with a tilde: \xc3\xb1", &output));
-}
-
-TEST_F(OmahaRequestActionTest, XmlEncodeWithDefaultTest) {
- EXPECT_EQ("<&>", XmlEncodeWithDefault("<&>", "something else"));
- EXPECT_EQ("<not escaped>", XmlEncodeWithDefault("\xc2", "<not escaped>"));
-}
-
TEST_F(OmahaRequestActionTest, XmlEncodeIsUsedForParams) {
brillo::Blob post_data;
diff --git a/omaha_request_builder_xml.cc b/omaha_request_builder_xml.cc
new file mode 100644
index 0000000..899f17f
--- /dev/null
+++ b/omaha_request_builder_xml.cc
@@ -0,0 +1,413 @@
+//
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#include "update_engine/omaha_request_builder_xml.h"
+
+#include <inttypes.h>
+
+#include <string>
+
+#include <base/logging.h>
+#include <base/strings/string_number_conversions.h>
+#include <base/strings/string_util.h>
+#include <base/strings/stringprintf.h>
+#include <base/time/time.h>
+
+#include "update_engine/common/constants.h"
+#include "update_engine/common/prefs_interface.h"
+#include "update_engine/common/utils.h"
+#include "update_engine/omaha_request_params.h"
+
+using std::string;
+
+namespace chromeos_update_engine {
+
+const int kNeverPinged = -1;
+
+bool XmlEncode(const string& input, string* output) {
+ if (std::find_if(input.begin(), input.end(), [](const char c) {
+ return c & 0x80;
+ }) != input.end()) {
+ LOG(WARNING) << "Invalid ASCII-7 string passed to the XML encoder:";
+ utils::HexDumpString(input);
+ return false;
+ }
+ output->clear();
+ // We need at least input.size() space in the output, but the code below will
+ // handle it if we need more.
+ output->reserve(input.size());
+ for (char c : input) {
+ switch (c) {
+ case '\"':
+ output->append(""");
+ break;
+ case '\'':
+ output->append("'");
+ break;
+ case '&':
+ output->append("&");
+ break;
+ case '<':
+ output->append("<");
+ break;
+ case '>':
+ output->append(">");
+ break;
+ default:
+ output->push_back(c);
+ }
+ }
+ return true;
+}
+
+string XmlEncodeWithDefault(const string& input, const string& default_value) {
+ string output;
+ if (XmlEncode(input, &output))
+ return output;
+ return default_value;
+}
+
+string GetPingAttribute(const string& name, int ping_days) {
+ if (ping_days > 0 || ping_days == kNeverPinged)
+ return base::StringPrintf(" %s=\"%d\"", name.c_str(), ping_days);
+ return "";
+}
+
+string GetPingXml(int ping_active_days, int ping_roll_call_days) {
+ string ping_active = GetPingAttribute("a", ping_active_days);
+ string ping_roll_call = GetPingAttribute("r", ping_roll_call_days);
+ if (!ping_active.empty() || !ping_roll_call.empty()) {
+ return base::StringPrintf(" <ping active=\"1\"%s%s></ping>\n",
+ ping_active.c_str(),
+ ping_roll_call.c_str());
+ }
+ return "";
+}
+
+string GetAppBody(const OmahaEvent* event,
+ OmahaRequestParams* params,
+ bool ping_only,
+ bool include_ping,
+ bool skip_updatecheck,
+ int ping_active_days,
+ int ping_roll_call_days,
+ PrefsInterface* prefs) {
+ string app_body;
+ if (event == nullptr) {
+ if (include_ping)
+ app_body = GetPingXml(ping_active_days, ping_roll_call_days);
+ if (!ping_only) {
+ if (!skip_updatecheck) {
+ app_body += " <updatecheck";
+ if (!params->target_version_prefix().empty()) {
+ app_body += base::StringPrintf(
+ " targetversionprefix=\"%s\"",
+ XmlEncodeWithDefault(params->target_version_prefix(), "")
+ .c_str());
+ // Rollback requires target_version_prefix set.
+ if (params->rollback_allowed()) {
+ app_body += " rollback_allowed=\"true\"";
+ }
+ }
+ app_body += "></updatecheck>\n";
+ }
+
+ // If this is the first update check after a reboot following a previous
+ // update, generate an event containing the previous version number. If
+ // the previous version preference file doesn't exist the event is still
+ // generated with a previous version of 0.0.0.0 -- this is relevant for
+ // older clients or new installs. The previous version event is not sent
+ // for ping-only requests because they come before the client has
+ // rebooted. The previous version event is also not sent if it was already
+ // sent for this new version with a previous updatecheck.
+ string prev_version;
+ if (!prefs->GetString(kPrefsPreviousVersion, &prev_version)) {
+ prev_version = "0.0.0.0";
+ }
+ // We only store a non-empty previous version value after a successful
+ // update in the previous boot. After reporting it back to the server,
+ // we clear the previous version value so it doesn't get reported again.
+ if (!prev_version.empty()) {
+ app_body += base::StringPrintf(
+ " <event eventtype=\"%d\" eventresult=\"%d\" "
+ "previousversion=\"%s\"></event>\n",
+ OmahaEvent::kTypeRebootedAfterUpdate,
+ OmahaEvent::kResultSuccess,
+ XmlEncodeWithDefault(prev_version, "0.0.0.0").c_str());
+ LOG_IF(WARNING, !prefs->SetString(kPrefsPreviousVersion, ""))
+ << "Unable to reset the previous version.";
+ }
+ }
+ } else {
+ // The error code is an optional attribute so append it only if the result
+ // is not success.
+ string error_code;
+ if (event->result != OmahaEvent::kResultSuccess) {
+ error_code = base::StringPrintf(" errorcode=\"%d\"",
+ static_cast<int>(event->error_code));
+ }
+ app_body = base::StringPrintf(
+ " <event eventtype=\"%d\" eventresult=\"%d\"%s></event>\n",
+ event->type,
+ event->result,
+ error_code.c_str());
+ }
+
+ return app_body;
+}
+
+string GetCohortArgXml(PrefsInterface* prefs,
+ const string arg_name,
+ const string prefs_key) {
+ // There's nothing wrong with not having a given cohort setting, so we check
+ // existence first to avoid the warning log message.
+ if (!prefs->Exists(prefs_key))
+ return "";
+ string cohort_value;
+ if (!prefs->GetString(prefs_key, &cohort_value) || cohort_value.empty())
+ return "";
+ // This is a sanity check to avoid sending a huge XML file back to Ohama due
+ // to a compromised stateful partition making the update check fail in low
+ // network environments envent after a reboot.
+ if (cohort_value.size() > 1024) {
+ LOG(WARNING) << "The omaha cohort setting " << arg_name
+ << " has a too big value, which must be an error or an "
+ "attacker trying to inhibit updates.";
+ return "";
+ }
+
+ string escaped_xml_value;
+ if (!XmlEncode(cohort_value, &escaped_xml_value)) {
+ LOG(WARNING) << "The omaha cohort setting " << arg_name
+ << " is ASCII-7 invalid, ignoring it.";
+ return "";
+ }
+
+ return base::StringPrintf(
+ "%s=\"%s\" ", arg_name.c_str(), escaped_xml_value.c_str());
+}
+
+bool IsValidComponentID(const string& id) {
+ for (char c : id) {
+ if (!isalnum(c) && c != '-' && c != '_' && c != '.')
+ return false;
+ }
+ return true;
+}
+
+string GetAppXml(const OmahaEvent* event,
+ OmahaRequestParams* params,
+ const OmahaAppData& app_data,
+ bool ping_only,
+ bool include_ping,
+ bool skip_updatecheck,
+ int ping_active_days,
+ int ping_roll_call_days,
+ int install_date_in_days,
+ SystemState* system_state) {
+ string app_body = GetAppBody(event,
+ params,
+ ping_only,
+ include_ping,
+ skip_updatecheck,
+ ping_active_days,
+ ping_roll_call_days,
+ system_state->prefs());
+ string app_versions;
+
+ // If we are downgrading to a more stable channel and we are allowed to do
+ // powerwash, then pass 0.0.0.0 as the version. This is needed to get the
+ // highest-versioned payload on the destination channel.
+ if (params->ShouldPowerwash()) {
+ LOG(INFO) << "Passing OS version as 0.0.0.0 as we are set to powerwash "
+ << "on downgrading to the version in the more stable channel";
+ app_versions = "version=\"0.0.0.0\" from_version=\"" +
+ XmlEncodeWithDefault(app_data.version, "0.0.0.0") + "\" ";
+ } else {
+ app_versions = "version=\"" +
+ XmlEncodeWithDefault(app_data.version, "0.0.0.0") + "\" ";
+ }
+
+ string download_channel = params->download_channel();
+ string app_channels =
+ "track=\"" + XmlEncodeWithDefault(download_channel, "") + "\" ";
+ if (params->current_channel() != download_channel) {
+ app_channels += "from_track=\"" +
+ XmlEncodeWithDefault(params->current_channel(), "") + "\" ";
+ }
+
+ string delta_okay_str = params->delta_okay() ? "true" : "false";
+
+ // If install_date_days is not set (e.g. its value is -1 ), don't
+ // include the attribute.
+ string install_date_in_days_str = "";
+ if (install_date_in_days >= 0) {
+ install_date_in_days_str =
+ base::StringPrintf("installdate=\"%d\" ", install_date_in_days);
+ }
+
+ string app_cohort_args;
+ app_cohort_args +=
+ GetCohortArgXml(system_state->prefs(), "cohort", kPrefsOmahaCohort);
+ app_cohort_args += GetCohortArgXml(
+ system_state->prefs(), "cohorthint", kPrefsOmahaCohortHint);
+ app_cohort_args += GetCohortArgXml(
+ system_state->prefs(), "cohortname", kPrefsOmahaCohortName);
+
+ string fingerprint_arg;
+ if (!params->os_build_fingerprint().empty()) {
+ fingerprint_arg = "fingerprint=\"" +
+ XmlEncodeWithDefault(params->os_build_fingerprint(), "") +
+ "\" ";
+ }
+
+ string buildtype_arg;
+ if (!params->os_build_type().empty()) {
+ buildtype_arg = "os_build_type=\"" +
+ XmlEncodeWithDefault(params->os_build_type(), "") + "\" ";
+ }
+
+ string product_components_args;
+ if (!params->ShouldPowerwash() && !app_data.product_components.empty()) {
+ brillo::KeyValueStore store;
+ if (store.LoadFromString(app_data.product_components)) {
+ for (const string& key : store.GetKeys()) {
+ if (!IsValidComponentID(key)) {
+ LOG(ERROR) << "Invalid component id: " << key;
+ continue;
+ }
+ string version;
+ if (!store.GetString(key, &version)) {
+ LOG(ERROR) << "Failed to get version for " << key
+ << " in product_components.";
+ continue;
+ }
+ product_components_args +=
+ base::StringPrintf("_%s.version=\"%s\" ",
+ key.c_str(),
+ XmlEncodeWithDefault(version, "").c_str());
+ }
+ } else {
+ LOG(ERROR) << "Failed to parse product_components:\n"
+ << app_data.product_components;
+ }
+ }
+
+ // clang-format off
+ string app_xml = " <app "
+ "appid=\"" + XmlEncodeWithDefault(app_data.id, "") + "\" " +
+ app_cohort_args +
+ app_versions +
+ app_channels +
+ product_components_args +
+ fingerprint_arg +
+ buildtype_arg +
+ "lang=\"" + XmlEncodeWithDefault(params->app_lang(), "en-US") + "\" " +
+ "board=\"" + XmlEncodeWithDefault(params->os_board(), "") + "\" " +
+ "hardware_class=\"" + XmlEncodeWithDefault(params->hwid(), "") + "\" " +
+ "delta_okay=\"" + delta_okay_str + "\" "
+ "fw_version=\"" + XmlEncodeWithDefault(params->fw_version(), "") + "\" " +
+ "ec_version=\"" + XmlEncodeWithDefault(params->ec_version(), "") + "\" " +
+ install_date_in_days_str +
+ ">\n" +
+ app_body +
+ " </app>\n";
+ // clang-format on
+ return app_xml;
+}
+
+string GetOsXml(OmahaRequestParams* params) {
+ string os_xml =
+ " <os "
+ "version=\"" +
+ XmlEncodeWithDefault(params->os_version(), "") + "\" " + "platform=\"" +
+ XmlEncodeWithDefault(params->os_platform(), "") + "\" " + "sp=\"" +
+ XmlEncodeWithDefault(params->os_sp(), "") +
+ "\">"
+ "</os>\n";
+ return os_xml;
+}
+
+string GetRequestXml(const OmahaEvent* event,
+ OmahaRequestParams* params,
+ bool ping_only,
+ bool include_ping,
+ int ping_active_days,
+ int ping_roll_call_days,
+ int install_date_in_days,
+ SystemState* system_state) {
+ string os_xml = GetOsXml(params);
+ OmahaAppData product_app = {
+ .id = params->GetAppId(),
+ .version = params->app_version(),
+ .product_components = params->product_components()};
+ // Skips updatecheck for platform app in case of an install operation.
+ string app_xml = GetAppXml(event,
+ params,
+ product_app,
+ ping_only,
+ include_ping,
+ params->is_install(), /* skip_updatecheck */
+ ping_active_days,
+ ping_roll_call_days,
+ install_date_in_days,
+ system_state);
+ if (!params->system_app_id().empty()) {
+ OmahaAppData system_app = {.id = params->system_app_id(),
+ .version = params->system_version()};
+ app_xml += GetAppXml(event,
+ params,
+ system_app,
+ ping_only,
+ include_ping,
+ false, /* skip_updatecheck */
+ ping_active_days,
+ ping_roll_call_days,
+ install_date_in_days,
+ system_state);
+ }
+ // Create APP ID according to |dlc_module_id| (sticking the current AppID to
+ // the DLC module ID with an underscode).
+ for (const auto& dlc_module_id : params->dlc_module_ids()) {
+ OmahaAppData dlc_module_app = {
+ .id = params->GetAppId() + "_" + dlc_module_id,
+ .version = params->app_version()};
+ app_xml += GetAppXml(event,
+ params,
+ dlc_module_app,
+ ping_only,
+ include_ping,
+ false, /* skip_updatecheck */
+ ping_active_days,
+ ping_roll_call_days,
+ install_date_in_days,
+ system_state);
+ }
+
+ string request_xml = base::StringPrintf(
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<request protocol=\"3.0\" updater=\"%s\" updaterversion=\"%s\""
+ " installsource=\"%s\" ismachine=\"1\">\n%s%s</request>\n",
+ constants::kOmahaUpdaterID,
+ kOmahaUpdaterVersion,
+ params->interactive() ? "ondemandupdate" : "scheduler",
+ os_xml.c_str(),
+ app_xml.c_str());
+
+ return request_xml;
+}
+
+} // namespace chromeos_update_engine
diff --git a/omaha_request_builder_xml.h b/omaha_request_builder_xml.h
new file mode 100644
index 0000000..011c592
--- /dev/null
+++ b/omaha_request_builder_xml.h
@@ -0,0 +1,161 @@
+//
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#ifndef UPDATE_ENGINE_OMAHA_REQUEST_BUILDER_XML_H_
+#define UPDATE_ENGINE_OMAHA_REQUEST_BUILDER_XML_H_
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest_prod.h> // for FRIEND_TEST
+
+#include <brillo/secure_blob.h>
+#include <curl/curl.h>
+
+#include "update_engine/common/action.h"
+#include "update_engine/common/http_fetcher.h"
+#include "update_engine/omaha_response.h"
+#include "update_engine/system_state.h"
+
+// TODO(ahassani): Make the xml builder into a class of its own so we don't have
+// to pass all these parameters around.
+
+namespace chromeos_update_engine {
+
+extern const int kNeverPinged;
+
+// This struct encapsulates the Omaha event information. For a
+// complete list of defined event types and results, see
+// http://code.google.com/p/omaha/wiki/ServerProtocol#event
+struct OmahaEvent {
+ // The Type values correspond to EVENT_TYPE values of Omaha.
+ enum Type {
+ kTypeUnknown = 0,
+ kTypeDownloadComplete = 1,
+ kTypeInstallComplete = 2,
+ kTypeUpdateComplete = 3,
+ kTypeUpdateDownloadStarted = 13,
+ kTypeUpdateDownloadFinished = 14,
+ // Chromium OS reserved type sent after the first reboot following an update
+ // completed.
+ kTypeRebootedAfterUpdate = 54,
+ };
+
+ // The Result values correspond to EVENT_RESULT values of Omaha.
+ enum Result {
+ kResultError = 0,
+ kResultSuccess = 1,
+ kResultUpdateDeferred = 9, // When we ignore/defer updates due to policy.
+ };
+
+ OmahaEvent()
+ : type(kTypeUnknown),
+ result(kResultError),
+ error_code(ErrorCode::kError) {}
+ explicit OmahaEvent(Type in_type)
+ : type(in_type),
+ result(kResultSuccess),
+ error_code(ErrorCode::kSuccess) {}
+ OmahaEvent(Type in_type, Result in_result, ErrorCode in_error_code)
+ : type(in_type), result(in_result), error_code(in_error_code) {}
+
+ Type type;
+ Result result;
+ ErrorCode error_code;
+};
+
+struct OmahaAppData {
+ std::string id;
+ std::string version;
+ std::string product_components;
+};
+
+// Encodes XML entities in a given string. Input must be ASCII-7 valid. If
+// the input is invalid, the default value is used instead.
+std::string XmlEncodeWithDefault(const std::string& input,
+ const std::string& default_value);
+
+// Escapes text so it can be included as character data and attribute
+// values. The |input| string must be valid ASCII-7, no UTF-8 supported.
+// Returns whether the |input| was valid and escaped properly in |output|.
+bool XmlEncode(const std::string& input, std::string* output);
+
+// Returns an XML ping element attribute assignment with attribute
+// |name| and value |ping_days| if |ping_days| has a value that needs
+// to be sent, or an empty string otherwise.
+std::string GetPingAttribute(const std::string& name, int ping_days);
+
+// Returns an XML ping element if any of the elapsed days need to be
+// sent, or an empty string otherwise.
+std::string GetPingXml(int ping_active_days, int ping_roll_call_days);
+
+// Returns an XML that goes into the body of the <app> element of the Omaha
+// request based on the given parameters.
+std::string GetAppBody(const OmahaEvent* event,
+ OmahaRequestParams* params,
+ bool ping_only,
+ bool include_ping,
+ bool skip_updatecheck,
+ int ping_active_days,
+ int ping_roll_call_days,
+ PrefsInterface* prefs);
+
+// Returns the cohort* argument to include in the <app> tag for the passed
+// |arg_name| and |prefs_key|, if any. The return value is suitable to
+// concatenate to the list of arguments and includes a space at the end.
+std::string GetCohortArgXml(PrefsInterface* prefs,
+ const std::string arg_name,
+ const std::string prefs_key);
+
+bool IsValidComponentID(const std::string& id);
+
+// Returns an XML that corresponds to the entire <app> node of the Omaha
+// request based on the given parameters.
+std::string GetAppXml(const OmahaEvent* event,
+ OmahaRequestParams* params,
+ const OmahaAppData& app_data,
+ bool ping_only,
+ bool include_ping,
+ bool skip_updatecheck,
+ int ping_active_days,
+ int ping_roll_call_days,
+ int install_date_in_days,
+ SystemState* system_state);
+
+// Returns an XML that corresponds to the entire <os> node of the Omaha
+// request based on the given parameters.
+std::string GetOsXml(OmahaRequestParams* params);
+
+// Returns an XML that corresponds to the entire Omaha request based on the
+// given parameters.
+std::string GetRequestXml(const OmahaEvent* event,
+ OmahaRequestParams* params,
+ bool ping_only,
+ bool include_ping,
+ int ping_active_days,
+ int ping_roll_call_days,
+ int install_date_in_days,
+ SystemState* system_state);
+
+} // namespace chromeos_update_engine
+
+#endif // UPDATE_ENGINE_OMAHA_REQUEST_BUILDER_XML_H_
diff --git a/omaha_request_builder_xml_unittest.cc b/omaha_request_builder_xml_unittest.cc
new file mode 100644
index 0000000..3293c44
--- /dev/null
+++ b/omaha_request_builder_xml_unittest.cc
@@ -0,0 +1,50 @@
+//
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#include "update_engine/omaha_request_builder_xml.h"
+
+#include <string>
+
+#include <gtest/gtest.h>
+
+using std::string;
+
+namespace chromeos_update_engine {
+
+class OmahaRequestBuilderXmlTest : public ::testing::Test {};
+
+TEST_F(OmahaRequestBuilderXmlTest, XmlEncodeTest) {
+ string output;
+ EXPECT_TRUE(XmlEncode("ab", &output));
+ EXPECT_EQ("ab", output);
+ EXPECT_TRUE(XmlEncode("a<b", &output));
+ EXPECT_EQ("a<b", output);
+ EXPECT_TRUE(XmlEncode("<&>\"\'\\", &output));
+ EXPECT_EQ("<&>"'\\", output);
+ EXPECT_TRUE(XmlEncode("<&>", &output));
+ EXPECT_EQ("&lt;&amp;&gt;", output);
+ // Check that unterminated UTF-8 strings are handled properly.
+ EXPECT_FALSE(XmlEncode("\xc2", &output));
+ // Fail with invalid ASCII-7 chars.
+ EXPECT_FALSE(XmlEncode("This is an 'n' with a tilde: \xc3\xb1", &output));
+}
+
+TEST_F(OmahaRequestBuilderXmlTest, XmlEncodeWithDefaultTest) {
+ EXPECT_EQ("<&>", XmlEncodeWithDefault("<&>", "something else"));
+ EXPECT_EQ("<not escaped>", XmlEncodeWithDefault("\xc2", "<not escaped>"));
+}
+
+} // namespace chromeos_update_engine
diff --git a/update_attempter.h b/update_attempter.h
index c106001..6c25eb2 100644
--- a/update_attempter.h
+++ b/update_attempter.h
@@ -37,6 +37,7 @@
#include "update_engine/common/action_processor.h"
#include "update_engine/common/cpu_limiter.h"
#include "update_engine/common/proxy_resolver.h"
+#include "update_engine/omaha_request_builder_xml.h"
#include "update_engine/omaha_request_params.h"
#include "update_engine/omaha_response_handler_action.h"
#include "update_engine/payload_consumer/download_action.h"
diff --git a/update_engine.gyp b/update_engine.gyp
index 754b314..b7ccae8 100644
--- a/update_engine.gyp
+++ b/update_engine.gyp
@@ -269,6 +269,7 @@
'metrics_reporter_omaha.cc',
'metrics_utils.cc',
'omaha_request_action.cc',
+ 'omaha_request_builder_xml.cc',
'omaha_request_params.cc',
'omaha_response_handler_action.cc',
'omaha_utils.cc',
@@ -424,8 +425,8 @@
'payload_generator/inplace_generator.cc',
'payload_generator/mapfile_filesystem.cc',
'payload_generator/payload_file.cc',
- 'payload_generator/payload_generation_config_chromeos.cc',
'payload_generator/payload_generation_config.cc',
+ 'payload_generator/payload_generation_config_chromeos.cc',
'payload_generator/payload_signer.cc',
'payload_generator/raw_filesystem.cc',
'payload_generator/squashfs_filesystem.cc',
@@ -564,6 +565,7 @@
'metrics_reporter_omaha_unittest.cc',
'metrics_utils_unittest.cc',
'omaha_request_action_unittest.cc',
+ 'omaha_request_builder_xml_unittest.cc',
'omaha_request_params_unittest.cc',
'omaha_response_handler_action_unittest.cc',
'omaha_utils_unittest.cc',