| Mike Frysinger | 8155d08 | 2012-04-06 15:23:18 -0400 | [diff] [blame] | 1 | // Copyright (c) 2012 The Chromium OS Authors. All rights reserved. | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 2 | // Use of this source code is governed by a BSD-style license that can be | 
|  | 3 | // found in the LICENSE file. | 
|  | 4 |  | 
| Darin Petkov | 6a5b322 | 2010-07-13 14:55:28 -0700 | [diff] [blame] | 5 | #include "update_engine/omaha_request_action.h" | 
| Darin Petkov | 85ced13 | 2010-09-01 10:20:56 -0700 | [diff] [blame] | 6 |  | 
| Andrew de los Reyes | 08c4e27 | 2010-04-15 14:02:17 -0700 | [diff] [blame] | 7 | #include <inttypes.h> | 
| Darin Petkov | 85ced13 | 2010-09-01 10:20:56 -0700 | [diff] [blame] | 8 |  | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 9 | #include <sstream> | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 10 | #include <string> | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 11 |  | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 12 | #include <base/logging.h> | 
|  | 13 | #include <base/rand_util.h> | 
| Darin Petkov | 85ced13 | 2010-09-01 10:20:56 -0700 | [diff] [blame] | 14 | #include <base/string_number_conversions.h> | 
|  | 15 | #include <base/string_util.h> | 
| Mike Frysinger | 8155d08 | 2012-04-06 15:23:18 -0400 | [diff] [blame] | 16 | #include <base/stringprintf.h> | 
| Darin Petkov | 85ced13 | 2010-09-01 10:20:56 -0700 | [diff] [blame] | 17 | #include <base/time.h> | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 18 | #include <libxml/xpath.h> | 
|  | 19 | #include <libxml/xpathInternals.h> | 
|  | 20 |  | 
|  | 21 | #include "update_engine/action_pipe.h" | 
| Darin Petkov | a4a8a8c | 2010-07-15 22:21:12 -0700 | [diff] [blame] | 22 | #include "update_engine/omaha_request_params.h" | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 23 | #include "update_engine/prefs_interface.h" | 
| adlr@google.com | c98a7ed | 2009-12-04 18:54:03 +0000 | [diff] [blame] | 24 | #include "update_engine/utils.h" | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 25 |  | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 26 | using base::Time; | 
|  | 27 | using base::TimeDelta; | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 28 | using std::string; | 
|  | 29 |  | 
|  | 30 | namespace chromeos_update_engine { | 
|  | 31 |  | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 32 | namespace { | 
|  | 33 |  | 
|  | 34 | const string kGupdateVersion("ChromeOSUpdateEngine-0.1.0.0"); | 
|  | 35 |  | 
|  | 36 | // This is handy for passing strings into libxml2 | 
|  | 37 | #define ConstXMLStr(x) (reinterpret_cast<const xmlChar*>(x)) | 
|  | 38 |  | 
|  | 39 | // These are for scoped_ptr_malloc, which is like scoped_ptr, but allows | 
|  | 40 | // a custom free() function to be specified. | 
|  | 41 | class ScopedPtrXmlDocFree { | 
|  | 42 | public: | 
|  | 43 | inline void operator()(void* x) const { | 
|  | 44 | xmlFreeDoc(reinterpret_cast<xmlDoc*>(x)); | 
|  | 45 | } | 
|  | 46 | }; | 
|  | 47 | class ScopedPtrXmlFree { | 
|  | 48 | public: | 
|  | 49 | inline void operator()(void* x) const { | 
|  | 50 | xmlFree(x); | 
|  | 51 | } | 
|  | 52 | }; | 
|  | 53 | class ScopedPtrXmlXPathObjectFree { | 
|  | 54 | public: | 
|  | 55 | inline void operator()(void* x) const { | 
|  | 56 | xmlXPathFreeObject(reinterpret_cast<xmlXPathObject*>(x)); | 
|  | 57 | } | 
|  | 58 | }; | 
|  | 59 | class ScopedPtrXmlXPathContextFree { | 
|  | 60 | public: | 
|  | 61 | inline void operator()(void* x) const { | 
|  | 62 | xmlXPathFreeContext(reinterpret_cast<xmlXPathContext*>(x)); | 
|  | 63 | } | 
|  | 64 | }; | 
|  | 65 |  | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 66 | // Returns true if |ping_days| has a value that needs to be sent, | 
|  | 67 | // false otherwise. | 
|  | 68 | bool ShouldPing(int ping_days) { | 
|  | 69 | return ping_days > 0 || ping_days == OmahaRequestAction::kNeverPinged; | 
|  | 70 | } | 
|  | 71 |  | 
|  | 72 | // Returns an XML ping element attribute assignment with attribute | 
|  | 73 | // |name| and value |ping_days| if |ping_days| has a value that needs | 
|  | 74 | // to be sent, or an empty string otherwise. | 
|  | 75 | string GetPingAttribute(const string& name, int ping_days) { | 
|  | 76 | if (ShouldPing(ping_days)) { | 
|  | 77 | return StringPrintf(" %s=\"%d\"", name.c_str(), ping_days); | 
|  | 78 | } | 
|  | 79 | return ""; | 
|  | 80 | } | 
|  | 81 |  | 
|  | 82 | // Returns an XML ping element if any of the elapsed days need to be | 
|  | 83 | // sent, or an empty string otherwise. | 
|  | 84 | string GetPingBody(int ping_active_days, int ping_roll_call_days) { | 
|  | 85 | string ping_active = GetPingAttribute("a", ping_active_days); | 
|  | 86 | string ping_roll_call = GetPingAttribute("r", ping_roll_call_days); | 
|  | 87 | if (!ping_active.empty() || !ping_roll_call.empty()) { | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 88 | return StringPrintf("        <ping active=\"1\"%s%s></ping>\n", | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 89 | ping_active.c_str(), | 
|  | 90 | ping_roll_call.c_str()); | 
|  | 91 | } | 
|  | 92 | return ""; | 
|  | 93 | } | 
|  | 94 |  | 
| Darin Petkov | 0dc8e9a | 2010-07-14 14:51:57 -0700 | [diff] [blame] | 95 | string FormatRequest(const OmahaEvent* event, | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 96 | const OmahaRequestParams& params, | 
| Thieu Le | 116fda3 | 2011-04-19 11:01:54 -0700 | [diff] [blame] | 97 | bool ping_only, | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 98 | int ping_active_days, | 
| Darin Petkov | 95508da | 2011-01-05 12:42:29 -0800 | [diff] [blame] | 99 | int ping_roll_call_days, | 
|  | 100 | PrefsInterface* prefs) { | 
| Darin Petkov | 0dc8e9a | 2010-07-14 14:51:57 -0700 | [diff] [blame] | 101 | string body; | 
|  | 102 | if (event == NULL) { | 
| Thieu Le | 116fda3 | 2011-04-19 11:01:54 -0700 | [diff] [blame] | 103 | body = GetPingBody(ping_active_days, ping_roll_call_days); | 
| Darin Petkov | 265f290 | 2011-05-09 15:17:40 -0700 | [diff] [blame] | 104 | if (!ping_only) { | 
| Jay Srinivasan | 56d5aa4 | 2012-03-26 14:27:59 -0700 | [diff] [blame] | 105 | // not passing update_disabled to Omaha because we want to | 
|  | 106 | // get the update and report with UpdateDeferred result so that | 
|  | 107 | // borgmon charts show up updates that are deferred. This is also | 
|  | 108 | // the expected behavior when we move to Omaha v3.0 protocol, so it'll | 
|  | 109 | // be consistent. | 
| Jay Srinivasan | 0a70874 | 2012-03-20 11:26:12 -0700 | [diff] [blame] | 110 | body += StringPrintf( | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 111 | "        <updatecheck" | 
| Jay Srinivasan | 0a70874 | 2012-03-20 11:26:12 -0700 | [diff] [blame] | 112 | " targetversionprefix=\"%s\"" | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 113 | "></updatecheck>\n", | 
| Jay Srinivasan | 0a70874 | 2012-03-20 11:26:12 -0700 | [diff] [blame] | 114 | XmlEncode(params.target_version_prefix).c_str()); | 
|  | 115 |  | 
| Darin Petkov | 265f290 | 2011-05-09 15:17:40 -0700 | [diff] [blame] | 116 | // If this is the first update check after a reboot following a previous | 
|  | 117 | // update, generate an event containing the previous version number. If | 
|  | 118 | // the previous version preference file doesn't exist the event is still | 
|  | 119 | // generated with a previous version of 0.0.0.0 -- this is relevant for | 
|  | 120 | // older clients or new installs. The previous version event is not sent | 
|  | 121 | // for ping-only requests because they come before the client has | 
|  | 122 | // rebooted. | 
|  | 123 | string prev_version; | 
|  | 124 | if (!prefs->GetString(kPrefsPreviousVersion, &prev_version)) { | 
|  | 125 | prev_version = "0.0.0.0"; | 
|  | 126 | } | 
|  | 127 | if (!prev_version.empty()) { | 
|  | 128 | body += StringPrintf( | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 129 | "        <event eventtype=\"%d\" eventresult=\"%d\" " | 
|  | 130 | "previousversion=\"%s\"></event>\n", | 
| Darin Petkov | 265f290 | 2011-05-09 15:17:40 -0700 | [diff] [blame] | 131 | OmahaEvent::kTypeUpdateComplete, | 
|  | 132 | OmahaEvent::kResultSuccessReboot, | 
|  | 133 | XmlEncode(prev_version).c_str()); | 
|  | 134 | LOG_IF(WARNING, !prefs->SetString(kPrefsPreviousVersion, "")) | 
|  | 135 | << "Unable to reset the previous version."; | 
|  | 136 | } | 
| Darin Petkov | 95508da | 2011-01-05 12:42:29 -0800 | [diff] [blame] | 137 | } | 
| Darin Petkov | 0dc8e9a | 2010-07-14 14:51:57 -0700 | [diff] [blame] | 138 | } else { | 
| Darin Petkov | c91dd6b | 2011-01-10 12:31:34 -0800 | [diff] [blame] | 139 | // The error code is an optional attribute so append it only if the result | 
|  | 140 | // is not success. | 
| Darin Petkov | e17f86b | 2010-07-20 09:12:01 -0700 | [diff] [blame] | 141 | string error_code; | 
|  | 142 | if (event->result != OmahaEvent::kResultSuccess) { | 
| Darin Petkov | 18c7bce | 2011-06-16 14:07:00 -0700 | [diff] [blame] | 143 | error_code = StringPrintf(" errorcode=\"%d\"", event->error_code); | 
| Darin Petkov | e17f86b | 2010-07-20 09:12:01 -0700 | [diff] [blame] | 144 | } | 
| Darin Petkov | 0dc8e9a | 2010-07-14 14:51:57 -0700 | [diff] [blame] | 145 | body = StringPrintf( | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 146 | "        <event eventtype=\"%d\" eventresult=\"%d\"%s></event>\n", | 
| Darin Petkov | e17f86b | 2010-07-20 09:12:01 -0700 | [diff] [blame] | 147 | event->type, event->result, error_code.c_str()); | 
| Darin Petkov | 0dc8e9a | 2010-07-14 14:51:57 -0700 | [diff] [blame] | 148 | } | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 149 | return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 150 | "<request protocol=\"3.0\" " | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 151 | "version=\"" + XmlEncode(kGupdateVersion) + "\" " | 
|  | 152 | "updaterversion=\"" + XmlEncode(kGupdateVersion) + "\" " | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 153 | "ismachine=\"1\">\n" | 
|  | 154 | "    <os version=\"" + XmlEncode(params.os_version) + "\" platform=\"" + | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 155 | XmlEncode(params.os_platform) + "\" sp=\"" + | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 156 | XmlEncode(params.os_sp) + "\"></os>\n" | 
|  | 157 | "    <app appid=\"" + XmlEncode(params.app_id) + "\" version=\"" + | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 158 | XmlEncode(params.app_version) + "\" " | 
|  | 159 | "lang=\"" + XmlEncode(params.app_lang) + "\" track=\"" + | 
| Andrew de los Reyes | 37c2032 | 2010-06-30 13:27:19 -0700 | [diff] [blame] | 160 | XmlEncode(params.app_track) + "\" board=\"" + | 
| Darin Petkov | fbb4009 | 2010-07-29 17:05:50 -0700 | [diff] [blame] | 161 | XmlEncode(params.os_board) + "\" hardware_class=\"" + | 
|  | 162 | XmlEncode(params.hardware_class) + "\" delta_okay=\"" + | 
| Andrew de los Reyes | 3f0303a | 2010-07-15 22:35:35 -0700 | [diff] [blame] | 163 | (params.delta_okay ? "true" : "false") + "\">\n" + body + | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 164 | "    </app>\n" | 
|  | 165 | "</request>\n"; | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 166 | } | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 167 |  | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 168 | }  // namespace {} | 
|  | 169 |  | 
|  | 170 | // Encodes XML entities in a given string with libxml2. input must be | 
|  | 171 | // UTF-8 formatted. Output will be UTF-8 formatted. | 
|  | 172 | string XmlEncode(const string& input) { | 
| Darin Petkov | 6a5b322 | 2010-07-13 14:55:28 -0700 | [diff] [blame] | 173 | //  // TODO(adlr): if allocating a new xmlDoc each time is taking up too much | 
|  | 174 | //  // cpu, considering creating one and caching it. | 
|  | 175 | //  scoped_ptr_malloc<xmlDoc, ScopedPtrXmlDocFree> xml_doc( | 
|  | 176 | //      xmlNewDoc(ConstXMLStr("1.0"))); | 
|  | 177 | //  if (!xml_doc.get()) { | 
|  | 178 | //    LOG(ERROR) << "Unable to create xmlDoc"; | 
|  | 179 | //    return ""; | 
|  | 180 | //  } | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 181 | scoped_ptr_malloc<xmlChar, ScopedPtrXmlFree> str( | 
|  | 182 | xmlEncodeEntitiesReentrant(NULL, ConstXMLStr(input.c_str()))); | 
|  | 183 | return string(reinterpret_cast<const char *>(str.get())); | 
|  | 184 | } | 
|  | 185 |  | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 186 | OmahaRequestAction::OmahaRequestAction(SystemState* system_state, | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 187 | OmahaRequestParams* params, | 
| Darin Petkov | a4a8a8c | 2010-07-15 22:21:12 -0700 | [diff] [blame] | 188 | OmahaEvent* event, | 
| Thieu Le | 116fda3 | 2011-04-19 11:01:54 -0700 | [diff] [blame] | 189 | HttpFetcher* http_fetcher, | 
|  | 190 | bool ping_only) | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 191 | : system_state_(system_state), | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 192 | params_(params), | 
| Darin Petkov | a4a8a8c | 2010-07-15 22:21:12 -0700 | [diff] [blame] | 193 | event_(event), | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 194 | http_fetcher_(http_fetcher), | 
| Thieu Le | 116fda3 | 2011-04-19 11:01:54 -0700 | [diff] [blame] | 195 | ping_only_(ping_only), | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 196 | ping_active_days_(0), | 
| Andrew de los Reyes | 771e1bd | 2011-08-30 14:47:23 -0700 | [diff] [blame] | 197 | ping_roll_call_days_(0) {} | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 198 |  | 
| Darin Petkov | 6a5b322 | 2010-07-13 14:55:28 -0700 | [diff] [blame] | 199 | OmahaRequestAction::~OmahaRequestAction() {} | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 200 |  | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 201 | // Calculates the value to use for the ping days parameter. | 
|  | 202 | int OmahaRequestAction::CalculatePingDays(const string& key) { | 
|  | 203 | int days = kNeverPinged; | 
|  | 204 | int64_t last_ping = 0; | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 205 | if (system_state_->prefs()->GetInt64(key, &last_ping) && last_ping >= 0) { | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 206 | days = (Time::Now() - Time::FromInternalValue(last_ping)).InDays(); | 
|  | 207 | if (days < 0) { | 
|  | 208 | // If |days| is negative, then the system clock must have jumped | 
|  | 209 | // back in time since the ping was sent. Mark the value so that | 
|  | 210 | // it doesn't get sent to the server but we still update the | 
|  | 211 | // last ping daystart preference. This way the next ping time | 
|  | 212 | // will be correct, hopefully. | 
|  | 213 | days = kPingTimeJump; | 
|  | 214 | LOG(WARNING) << | 
|  | 215 | "System clock jumped back in time. Resetting ping daystarts."; | 
|  | 216 | } | 
|  | 217 | } | 
|  | 218 | return days; | 
|  | 219 | } | 
|  | 220 |  | 
|  | 221 | void OmahaRequestAction::InitPingDays() { | 
|  | 222 | // We send pings only along with update checks, not with events. | 
|  | 223 | if (IsEvent()) { | 
|  | 224 | return; | 
|  | 225 | } | 
|  | 226 | // TODO(petkov): Figure a way to distinguish active use pings | 
|  | 227 | // vs. roll call pings. Currently, the two pings are identical. A | 
|  | 228 | // fix needs to change this code as well as UpdateLastPingDays. | 
|  | 229 | ping_active_days_ = CalculatePingDays(kPrefsLastActivePingDay); | 
|  | 230 | ping_roll_call_days_ = CalculatePingDays(kPrefsLastRollCallPingDay); | 
|  | 231 | } | 
|  | 232 |  | 
| Darin Petkov | 6a5b322 | 2010-07-13 14:55:28 -0700 | [diff] [blame] | 233 | void OmahaRequestAction::PerformAction() { | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 234 | http_fetcher_->set_delegate(this); | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 235 | InitPingDays(); | 
| Thieu Le | b44e9e8 | 2011-06-06 14:34:04 -0700 | [diff] [blame] | 236 | if (ping_only_ && | 
|  | 237 | !ShouldPing(ping_active_days_) && | 
|  | 238 | !ShouldPing(ping_roll_call_days_)) { | 
|  | 239 | processor_->ActionComplete(this, kActionCodeSuccess); | 
|  | 240 | return; | 
|  | 241 | } | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 242 | string request_post(FormatRequest(event_.get(), | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 243 | *params_, | 
| Thieu Le | 116fda3 | 2011-04-19 11:01:54 -0700 | [diff] [blame] | 244 | ping_only_, | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 245 | ping_active_days_, | 
| Darin Petkov | 95508da | 2011-01-05 12:42:29 -0800 | [diff] [blame] | 246 | ping_roll_call_days_, | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 247 | system_state_->prefs())); | 
| Jay Srinivasan | 0a70874 | 2012-03-20 11:26:12 -0700 | [diff] [blame] | 248 |  | 
| Gilad Arnold | 9dd1e7c | 2012-02-16 12:13:36 -0800 | [diff] [blame] | 249 | http_fetcher_->SetPostData(request_post.data(), request_post.size(), | 
|  | 250 | kHttpContentTypeTextXml); | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 251 | LOG(INFO) << "Posting an Omaha request to " << params_->update_url; | 
| Andrew de los Reyes | f98bff8 | 2010-05-06 13:33:25 -0700 | [diff] [blame] | 252 | LOG(INFO) << "Request: " << request_post; | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 253 | http_fetcher_->BeginTransfer(params_->update_url); | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 254 | } | 
|  | 255 |  | 
| Darin Petkov | 6a5b322 | 2010-07-13 14:55:28 -0700 | [diff] [blame] | 256 | void OmahaRequestAction::TerminateProcessing() { | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 257 | http_fetcher_->TerminateTransfer(); | 
|  | 258 | } | 
|  | 259 |  | 
|  | 260 | // We just store the response in the buffer. Once we've received all bytes, | 
|  | 261 | // we'll look in the buffer and decide what to do. | 
| Darin Petkov | 6a5b322 | 2010-07-13 14:55:28 -0700 | [diff] [blame] | 262 | void OmahaRequestAction::ReceivedBytes(HttpFetcher *fetcher, | 
|  | 263 | const char* bytes, | 
|  | 264 | int length) { | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 265 | response_buffer_.reserve(response_buffer_.size() + length); | 
|  | 266 | response_buffer_.insert(response_buffer_.end(), bytes, bytes + length); | 
|  | 267 | } | 
|  | 268 |  | 
|  | 269 | namespace { | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 270 | // If non-NULL response, caller is responsible for calling xmlXPathFreeObject() | 
|  | 271 | // on the returned object. | 
|  | 272 | // This code is roughly based on the libxml tutorial at: | 
|  | 273 | // http://xmlsoft.org/tutorial/apd.html | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 274 | xmlXPathObject* GetNodeSet(xmlDoc* doc, const xmlChar* xpath) { | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 275 | xmlXPathObject* result = NULL; | 
|  | 276 |  | 
|  | 277 | scoped_ptr_malloc<xmlXPathContext, ScopedPtrXmlXPathContextFree> context( | 
|  | 278 | xmlXPathNewContext(doc)); | 
|  | 279 | if (!context.get()) { | 
|  | 280 | LOG(ERROR) << "xmlXPathNewContext() returned NULL"; | 
|  | 281 | return NULL; | 
|  | 282 | } | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 283 |  | 
|  | 284 | result = xmlXPathEvalExpression(xpath, context.get()); | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 285 | if (result == NULL) { | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 286 | LOG(ERROR) << "Unable to find " << xpath << " in XML document"; | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 287 | return NULL; | 
|  | 288 | } | 
|  | 289 | if(xmlXPathNodeSetIsEmpty(result->nodesetval)){ | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 290 | LOG(INFO) << "Nodeset is empty for " << xpath; | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 291 | xmlXPathFreeObject(result); | 
|  | 292 | return NULL; | 
|  | 293 | } | 
|  | 294 | return result; | 
|  | 295 | } | 
|  | 296 |  | 
|  | 297 | // Returns the string value of a named attribute on a node, or empty string | 
|  | 298 | // if no such node exists. If the attribute exists and has a value of | 
|  | 299 | // empty string, there's no way to distinguish that from the attribute | 
|  | 300 | // not existing. | 
|  | 301 | string XmlGetProperty(xmlNode* node, const char* name) { | 
|  | 302 | if (!xmlHasProp(node, ConstXMLStr(name))) | 
|  | 303 | return ""; | 
|  | 304 | scoped_ptr_malloc<xmlChar, ScopedPtrXmlFree> str( | 
|  | 305 | xmlGetProp(node, ConstXMLStr(name))); | 
|  | 306 | string ret(reinterpret_cast<const char *>(str.get())); | 
|  | 307 | return ret; | 
|  | 308 | } | 
|  | 309 |  | 
|  | 310 | // Parses a 64 bit base-10 int from a string and returns it. Returns 0 | 
|  | 311 | // on error. If the string contains "0", that's indistinguishable from | 
|  | 312 | // error. | 
|  | 313 | off_t ParseInt(const string& str) { | 
|  | 314 | off_t ret = 0; | 
| Andrew de los Reyes | 08c4e27 | 2010-04-15 14:02:17 -0700 | [diff] [blame] | 315 | int rc = sscanf(str.c_str(), "%" PRIi64, &ret); | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 316 | if (rc < 1) { | 
|  | 317 | // failure | 
|  | 318 | return 0; | 
|  | 319 | } | 
|  | 320 | return ret; | 
|  | 321 | } | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 322 |  | 
|  | 323 | // Update the last ping day preferences based on the server daystart | 
|  | 324 | // response. Returns true on success, false otherwise. | 
|  | 325 | bool UpdateLastPingDays(xmlDoc* doc, PrefsInterface* prefs) { | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 326 | static const char kDaystartNodeXpath[] = "/response/daystart"; | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 327 |  | 
|  | 328 | scoped_ptr_malloc<xmlXPathObject, ScopedPtrXmlXPathObjectFree> | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 329 | xpath_nodeset(GetNodeSet(doc, ConstXMLStr(kDaystartNodeXpath))); | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 330 | TEST_AND_RETURN_FALSE(xpath_nodeset.get()); | 
|  | 331 | xmlNodeSet* nodeset = xpath_nodeset->nodesetval; | 
|  | 332 | TEST_AND_RETURN_FALSE(nodeset && nodeset->nodeNr >= 1); | 
|  | 333 | xmlNode* daystart_node = nodeset->nodeTab[0]; | 
|  | 334 | TEST_AND_RETURN_FALSE(xmlHasProp(daystart_node, | 
|  | 335 | ConstXMLStr("elapsed_seconds"))); | 
|  | 336 |  | 
|  | 337 | int64_t elapsed_seconds = 0; | 
| Chris Masone | 790e62e | 2010-08-12 10:41:18 -0700 | [diff] [blame] | 338 | TEST_AND_RETURN_FALSE(base::StringToInt64(XmlGetProperty(daystart_node, | 
|  | 339 | "elapsed_seconds"), | 
|  | 340 | &elapsed_seconds)); | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 341 | TEST_AND_RETURN_FALSE(elapsed_seconds >= 0); | 
|  | 342 |  | 
|  | 343 | // Remember the local time that matches the server's last midnight | 
|  | 344 | // time. | 
|  | 345 | Time daystart = Time::Now() - TimeDelta::FromSeconds(elapsed_seconds); | 
|  | 346 | prefs->SetInt64(kPrefsLastActivePingDay, daystart.ToInternalValue()); | 
|  | 347 | prefs->SetInt64(kPrefsLastRollCallPingDay, daystart.ToInternalValue()); | 
|  | 348 | return true; | 
|  | 349 | } | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 350 | }  // namespace {} | 
|  | 351 |  | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 352 | bool OmahaRequestAction::ParseResponse(xmlDoc* doc, | 
|  | 353 | OmahaResponse* output_object, | 
|  | 354 | ScopedActionCompleter* completer) { | 
|  | 355 | static const char* kUpdatecheckNodeXpath("/response/app/updatecheck"); | 
|  | 356 |  | 
|  | 357 | scoped_ptr_malloc<xmlXPathObject, ScopedPtrXmlXPathObjectFree> | 
|  | 358 | xpath_nodeset(GetNodeSet(doc, ConstXMLStr(kUpdatecheckNodeXpath))); | 
|  | 359 | if (!xpath_nodeset.get()) { | 
|  | 360 | completer->set_code(kActionCodeOmahaResponseInvalid); | 
|  | 361 | return false; | 
|  | 362 | } | 
|  | 363 |  | 
|  | 364 | xmlNodeSet* nodeset = xpath_nodeset->nodesetval; | 
|  | 365 | CHECK(nodeset) << "XPath missing UpdateCheck NodeSet"; | 
|  | 366 | CHECK_GE(nodeset->nodeNr, 1); | 
|  | 367 | xmlNode* update_check_node = nodeset->nodeTab[0]; | 
|  | 368 |  | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 369 | // chromium-os:37289: The PollInterval is not supported by Omaha server | 
|  | 370 | // currently.  But still keeping this existing code in case we ever decide to | 
|  | 371 | // slow down the request rate from the server-side. Note that the | 
|  | 372 | // PollInterval is not persisted, so it has to be sent by the server on every | 
|  | 373 | // response to guarantee that the UpdateCheckScheduler uses this value | 
|  | 374 | // (otherwise, if the device got rebooted after the last server-indicated | 
|  | 375 | // value, it'll revert to the default value). Also kDefaultMaxUpdateChecks | 
|  | 376 | // value for the scattering logic is based on the assumption that we perform | 
|  | 377 | // an update check every hour so that the max value of 8 will roughly be | 
|  | 378 | // equivalent to one work day. If we decide to use PollInterval permanently, | 
|  | 379 | // we should update the max_update_checks_allowed to take PollInterval into | 
|  | 380 | // account.  Note: The parsing for PollInterval happens even before parsing | 
|  | 381 | // of the status because we may want to specify the PollInterval even when | 
|  | 382 | // there's no update. | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 383 | base::StringToInt(XmlGetProperty(update_check_node, "PollInterval"), | 
|  | 384 | &output_object->poll_interval); | 
|  | 385 |  | 
|  | 386 | if (!ParseStatus(update_check_node, output_object, completer)) | 
|  | 387 | return false; | 
|  | 388 |  | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 389 | // Note: ParseUrls MUST be called before ParsePackage as ParsePackage | 
|  | 390 | // appends the package name to the URLs populated in this method. | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 391 | if (!ParseUrls(doc, output_object, completer)) | 
|  | 392 | return false; | 
|  | 393 |  | 
|  | 394 | if (!ParsePackage(doc, output_object, completer)) | 
|  | 395 | return false; | 
|  | 396 |  | 
|  | 397 | if (!ParseParams(doc, output_object, completer)) | 
|  | 398 | return false; | 
|  | 399 |  | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 400 | output_object->update_exists = true; | 
|  | 401 | SetOutputObject(*output_object); | 
|  | 402 | completer->set_code(kActionCodeSuccess); | 
|  | 403 |  | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 404 | return true; | 
|  | 405 | } | 
|  | 406 |  | 
|  | 407 | bool OmahaRequestAction::ParseStatus(xmlNode* update_check_node, | 
|  | 408 | OmahaResponse* output_object, | 
|  | 409 | ScopedActionCompleter* completer) { | 
|  | 410 | // Get status. | 
|  | 411 | if (!xmlHasProp(update_check_node, ConstXMLStr("status"))) { | 
|  | 412 | LOG(ERROR) << "Omaha Response missing status"; | 
|  | 413 | completer->set_code(kActionCodeOmahaResponseInvalid); | 
|  | 414 | return false; | 
|  | 415 | } | 
|  | 416 |  | 
|  | 417 | const string status(XmlGetProperty(update_check_node, "status")); | 
|  | 418 | if (status == "noupdate") { | 
|  | 419 | LOG(INFO) << "No update."; | 
|  | 420 | output_object->update_exists = false; | 
|  | 421 | SetOutputObject(*output_object); | 
|  | 422 | completer->set_code(kActionCodeSuccess); | 
|  | 423 | return false; | 
|  | 424 | } | 
|  | 425 |  | 
|  | 426 | if (status != "ok") { | 
|  | 427 | LOG(ERROR) << "Unknown Omaha response status: " << status; | 
|  | 428 | completer->set_code(kActionCodeOmahaResponseInvalid); | 
|  | 429 | return false; | 
|  | 430 | } | 
|  | 431 |  | 
|  | 432 | return true; | 
|  | 433 | } | 
|  | 434 |  | 
|  | 435 | bool OmahaRequestAction::ParseUrls(xmlDoc* doc, | 
|  | 436 | OmahaResponse* output_object, | 
|  | 437 | ScopedActionCompleter* completer) { | 
|  | 438 | // Get the update URL. | 
|  | 439 | static const char* kUpdateUrlNodeXPath("/response/app/updatecheck/urls/url"); | 
|  | 440 |  | 
|  | 441 | scoped_ptr_malloc<xmlXPathObject, ScopedPtrXmlXPathObjectFree> | 
|  | 442 | xpath_nodeset(GetNodeSet(doc, ConstXMLStr(kUpdateUrlNodeXPath))); | 
|  | 443 | if (!xpath_nodeset.get()) { | 
|  | 444 | completer->set_code(kActionCodeOmahaResponseInvalid); | 
|  | 445 | return false; | 
|  | 446 | } | 
|  | 447 |  | 
|  | 448 | xmlNodeSet* nodeset = xpath_nodeset->nodesetval; | 
|  | 449 | CHECK(nodeset) << "XPath missing " << kUpdateUrlNodeXPath; | 
|  | 450 | CHECK_GE(nodeset->nodeNr, 1); | 
|  | 451 |  | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 452 | LOG(INFO) << "Found " << nodeset->nodeNr << " url(s)"; | 
|  | 453 | output_object->payload_urls.clear(); | 
|  | 454 | for (int i = 0; i < nodeset->nodeNr; i++) { | 
|  | 455 | xmlNode* url_node = nodeset->nodeTab[i]; | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 456 |  | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 457 | const string codebase(XmlGetProperty(url_node, "codebase")); | 
|  | 458 | if (codebase.empty()) { | 
|  | 459 | LOG(ERROR) << "Omaha Response URL has empty codebase"; | 
|  | 460 | completer->set_code(kActionCodeOmahaResponseInvalid); | 
|  | 461 | return false; | 
|  | 462 | } | 
|  | 463 | output_object->payload_urls.push_back(codebase); | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 464 | } | 
|  | 465 |  | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 466 | return true; | 
|  | 467 | } | 
|  | 468 |  | 
|  | 469 | bool OmahaRequestAction::ParsePackage(xmlDoc* doc, | 
|  | 470 | OmahaResponse* output_object, | 
|  | 471 | ScopedActionCompleter* completer) { | 
|  | 472 | // Get the package node. | 
|  | 473 | static const char* kPackageNodeXPath( | 
|  | 474 | "/response/app/updatecheck/manifest/packages/package"); | 
|  | 475 |  | 
|  | 476 | scoped_ptr_malloc<xmlXPathObject, ScopedPtrXmlXPathObjectFree> | 
|  | 477 | xpath_nodeset(GetNodeSet(doc, ConstXMLStr(kPackageNodeXPath))); | 
|  | 478 | if (!xpath_nodeset.get()) { | 
|  | 479 | completer->set_code(kActionCodeOmahaResponseInvalid); | 
|  | 480 | return false; | 
|  | 481 | } | 
|  | 482 |  | 
|  | 483 | xmlNodeSet* nodeset = xpath_nodeset->nodesetval; | 
|  | 484 | CHECK(nodeset) << "XPath missing " << kPackageNodeXPath; | 
|  | 485 | CHECK_GE(nodeset->nodeNr, 1); | 
|  | 486 |  | 
|  | 487 | // We only care about the first package. | 
|  | 488 | LOG(INFO) << "Processing first of " << nodeset->nodeNr << " package(s)"; | 
|  | 489 | xmlNode* package_node = nodeset->nodeTab[0]; | 
|  | 490 |  | 
|  | 491 | // Get package properties one by one. | 
|  | 492 |  | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 493 | // Parse the payload name to be appended to the base Url value. | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 494 | const string package_name(XmlGetProperty(package_node, "name")); | 
|  | 495 | LOG(INFO) << "Omaha Response package name = " << package_name; | 
|  | 496 | if (package_name.empty()) { | 
|  | 497 | LOG(ERROR) << "Omaha Response has empty package name"; | 
|  | 498 | completer->set_code(kActionCodeOmahaResponseInvalid); | 
|  | 499 | return false; | 
|  | 500 | } | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 501 |  | 
|  | 502 | // Append the package name to each URL in our list so that we don't | 
|  | 503 | // propagate the urlBase vs packageName distinctions beyond this point. | 
|  | 504 | // From now on, we only need to use payload_urls. | 
|  | 505 | for (size_t i = 0; i < output_object->payload_urls.size(); i++) { | 
|  | 506 | output_object->payload_urls[i] += package_name; | 
|  | 507 | LOG(INFO) << "Url" << i << ": " << output_object->payload_urls[i]; | 
|  | 508 | } | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 509 |  | 
|  | 510 | // Parse the payload size. | 
|  | 511 | off_t size = ParseInt(XmlGetProperty(package_node, "size")); | 
|  | 512 | if (size <= 0) { | 
|  | 513 | LOG(ERROR) << "Omaha Response has invalid payload size: " << size; | 
|  | 514 | completer->set_code(kActionCodeOmahaResponseInvalid); | 
|  | 515 | return false; | 
|  | 516 | } | 
|  | 517 | output_object->size = size; | 
|  | 518 |  | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 519 | LOG(INFO) << "Payload size = " << output_object->size << " bytes"; | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 520 |  | 
|  | 521 | return true; | 
|  | 522 | } | 
|  | 523 |  | 
|  | 524 | bool OmahaRequestAction::ParseParams(xmlDoc* doc, | 
|  | 525 | OmahaResponse* output_object, | 
|  | 526 | ScopedActionCompleter* completer) { | 
|  | 527 | // Get the action node where parameters are present. | 
|  | 528 | static const char* kActionNodeXPath( | 
|  | 529 | "/response/app/updatecheck/manifest/actions/action"); | 
|  | 530 |  | 
|  | 531 | scoped_ptr_malloc<xmlXPathObject, ScopedPtrXmlXPathObjectFree> | 
|  | 532 | xpath_nodeset(GetNodeSet(doc, ConstXMLStr(kActionNodeXPath))); | 
|  | 533 | if (!xpath_nodeset.get()) { | 
|  | 534 | completer->set_code(kActionCodeOmahaResponseInvalid); | 
|  | 535 | return false; | 
|  | 536 | } | 
|  | 537 |  | 
|  | 538 | xmlNodeSet* nodeset = xpath_nodeset->nodesetval; | 
|  | 539 | CHECK(nodeset) << "XPath missing " << kActionNodeXPath; | 
|  | 540 |  | 
|  | 541 | // We only care about the action that has event "postinall", because this is | 
|  | 542 | // where Omaha puts all the generic name/value pairs in the rule. | 
|  | 543 | LOG(INFO) << "Found " << nodeset->nodeNr | 
|  | 544 | << " action(s). Processing the postinstall action."; | 
|  | 545 |  | 
|  | 546 | // pie_action_node holds the action node corresponding to the | 
|  | 547 | // postinstall event action, if present. | 
|  | 548 | xmlNode* pie_action_node = NULL; | 
|  | 549 | for (int i = 0; i < nodeset->nodeNr; i++) { | 
|  | 550 | xmlNode* action_node = nodeset->nodeTab[i]; | 
|  | 551 | if (XmlGetProperty(action_node, "event") == "postinstall") { | 
|  | 552 | pie_action_node = action_node; | 
|  | 553 | break; | 
|  | 554 | } | 
|  | 555 | } | 
|  | 556 |  | 
|  | 557 | if (!pie_action_node) { | 
|  | 558 | LOG(ERROR) << "Omaha Response has no postinstall event action"; | 
|  | 559 | completer->set_code(kActionCodeOmahaResponseInvalid); | 
|  | 560 | return false; | 
|  | 561 | } | 
|  | 562 |  | 
|  | 563 | output_object->hash = XmlGetProperty(pie_action_node, "sha256"); | 
|  | 564 | if (output_object->hash.empty()) { | 
|  | 565 | LOG(ERROR) << "Omaha Response has empty sha256 value"; | 
|  | 566 | completer->set_code(kActionCodeOmahaResponseInvalid); | 
|  | 567 | return false; | 
|  | 568 | } | 
|  | 569 |  | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 570 | // Get the optional properties one by one. | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 571 | output_object->display_version = | 
|  | 572 | XmlGetProperty(pie_action_node, "DisplayVersion"); | 
|  | 573 | output_object->more_info_url = XmlGetProperty(pie_action_node, "MoreInfo"); | 
|  | 574 | output_object->metadata_size = | 
|  | 575 | ParseInt(XmlGetProperty(pie_action_node, "MetadataSize")); | 
|  | 576 | output_object->metadata_signature = | 
|  | 577 | XmlGetProperty(pie_action_node, "MetadataSignatureRsa"); | 
|  | 578 | output_object->needs_admin = | 
|  | 579 | XmlGetProperty(pie_action_node, "needsadmin") == "true"; | 
|  | 580 | output_object->prompt = XmlGetProperty(pie_action_node, "Prompt") == "true"; | 
|  | 581 | output_object->deadline = XmlGetProperty(pie_action_node, "deadline"); | 
|  | 582 | output_object->max_days_to_scatter = | 
|  | 583 | ParseInt(XmlGetProperty(pie_action_node, "MaxDaysToScatter")); | 
|  | 584 |  | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 585 | return true; | 
|  | 586 | } | 
|  | 587 |  | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 588 | // If the transfer was successful, this uses libxml2 to parse the response | 
|  | 589 | // and fill in the appropriate fields of the output object. Also, notifies | 
|  | 590 | // the processor that we're done. | 
| Darin Petkov | 6a5b322 | 2010-07-13 14:55:28 -0700 | [diff] [blame] | 591 | void OmahaRequestAction::TransferComplete(HttpFetcher *fetcher, | 
|  | 592 | bool successful) { | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 593 | ScopedActionCompleter completer(processor_, this); | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 594 | string current_response(response_buffer_.begin(), response_buffer_.end()); | 
|  | 595 | LOG(INFO) << "Omaha request response: " << current_response; | 
| Darin Petkov | 0dc8e9a | 2010-07-14 14:51:57 -0700 | [diff] [blame] | 596 |  | 
|  | 597 | // Events are best effort transactions -- assume they always succeed. | 
|  | 598 | if (IsEvent()) { | 
|  | 599 | CHECK(!HasOutputPipe()) << "No output pipe allowed for event requests."; | 
| Andrew de los Reyes | 2008e4c | 2011-01-12 10:17:52 -0800 | [diff] [blame] | 600 | if (event_->result == OmahaEvent::kResultError && successful && | 
|  | 601 | utils::IsOfficialBuild()) { | 
|  | 602 | LOG(INFO) << "Signalling Crash Reporter."; | 
|  | 603 | utils::ScheduleCrashReporterUpload(); | 
|  | 604 | } | 
| Darin Petkov | c1a8b42 | 2010-07-19 11:34:49 -0700 | [diff] [blame] | 605 | completer.set_code(kActionCodeSuccess); | 
| Darin Petkov | 0dc8e9a | 2010-07-14 14:51:57 -0700 | [diff] [blame] | 606 | return; | 
|  | 607 | } | 
|  | 608 |  | 
| Andrew de los Reyes | f98bff8 | 2010-05-06 13:33:25 -0700 | [diff] [blame] | 609 | if (!successful) { | 
| Darin Petkov | 0dc8e9a | 2010-07-14 14:51:57 -0700 | [diff] [blame] | 610 | LOG(ERROR) << "Omaha request network transfer failed."; | 
| Darin Petkov | edc522e | 2010-11-05 09:35:17 -0700 | [diff] [blame] | 611 | int code = GetHTTPResponseCode(); | 
|  | 612 | // Makes sure we send sane error values. | 
|  | 613 | if (code < 0 || code >= 1000) { | 
|  | 614 | code = 999; | 
|  | 615 | } | 
|  | 616 | completer.set_code(static_cast<ActionExitCode>( | 
|  | 617 | kActionCodeOmahaRequestHTTPResponseBase + code)); | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 618 | return; | 
| Andrew de los Reyes | f98bff8 | 2010-05-06 13:33:25 -0700 | [diff] [blame] | 619 | } | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 620 |  | 
|  | 621 | // parse our response and fill the fields in the output object | 
|  | 622 | scoped_ptr_malloc<xmlDoc, ScopedPtrXmlDocFree> doc( | 
|  | 623 | xmlParseMemory(&response_buffer_[0], response_buffer_.size())); | 
|  | 624 | if (!doc.get()) { | 
|  | 625 | LOG(ERROR) << "Omaha response not valid XML"; | 
| Darin Petkov | edc522e | 2010-11-05 09:35:17 -0700 | [diff] [blame] | 626 | completer.set_code(response_buffer_.empty() ? | 
|  | 627 | kActionCodeOmahaRequestEmptyResponseError : | 
|  | 628 | kActionCodeOmahaRequestXMLParseError); | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 629 | return; | 
|  | 630 | } | 
|  | 631 |  | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 632 | // If a ping was sent, update the last ping day preferences based on | 
|  | 633 | // the server daystart response. | 
|  | 634 | if (ShouldPing(ping_active_days_) || | 
|  | 635 | ShouldPing(ping_roll_call_days_) || | 
|  | 636 | ping_active_days_ == kPingTimeJump || | 
|  | 637 | ping_roll_call_days_ == kPingTimeJump) { | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 638 | LOG_IF(ERROR, !UpdateLastPingDays(doc.get(), system_state_->prefs())) | 
| Darin Petkov | 1cbd78f | 2010-07-29 12:38:34 -0700 | [diff] [blame] | 639 | << "Failed to update the last ping day preferences!"; | 
|  | 640 | } | 
|  | 641 |  | 
| Thieu Le | 116fda3 | 2011-04-19 11:01:54 -0700 | [diff] [blame] | 642 | if (!HasOutputPipe()) { | 
|  | 643 | // Just set success to whether or not the http transfer succeeded, | 
|  | 644 | // which must be true at this point in the code. | 
|  | 645 | completer.set_code(kActionCodeSuccess); | 
|  | 646 | return; | 
|  | 647 | } | 
|  | 648 |  | 
| Darin Petkov | 6a5b322 | 2010-07-13 14:55:28 -0700 | [diff] [blame] | 649 | OmahaResponse output_object; | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 650 | if (!ParseResponse(doc.get(), &output_object, &completer)) | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 651 | return; | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 652 |  | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 653 | if (params_->update_disabled) { | 
| Jay Srinivasan | 56d5aa4 | 2012-03-26 14:27:59 -0700 | [diff] [blame] | 654 | LOG(INFO) << "Ignoring Omaha updates as updates are disabled by policy."; | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 655 | output_object.update_exists = false; | 
| Jay Srinivasan | 0a70874 | 2012-03-20 11:26:12 -0700 | [diff] [blame] | 656 | completer.set_code(kActionCodeOmahaUpdateIgnoredPerPolicy); | 
| Jay Srinivasan | 34b5d86 | 2012-07-23 11:43:22 -0700 | [diff] [blame] | 657 | // Note: We could technically delete the UpdateFirstSeenAt state here. | 
|  | 658 | // If we do, it'll mean a device has to restart the UpdateFirstSeenAt | 
|  | 659 | // and thus help scattering take effect when the AU is turned on again. | 
|  | 660 | // On the other hand, it also increases the chance of update starvation if | 
|  | 661 | // an admin turns AU on/off more frequently. We choose to err on the side | 
|  | 662 | // of preventing starvation at the cost of not applying scattering in | 
|  | 663 | // those cases. | 
| Jay Srinivasan | 0a70874 | 2012-03-20 11:26:12 -0700 | [diff] [blame] | 664 | return; | 
|  | 665 | } | 
|  | 666 |  | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 667 | if (ShouldDeferDownload(&output_object)) { | 
|  | 668 | output_object.update_exists = false; | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 669 | LOG(INFO) << "Ignoring Omaha updates as updates are deferred by policy."; | 
|  | 670 | completer.set_code(kActionCodeOmahaUpdateDeferredPerPolicy); | 
|  | 671 | return; | 
|  | 672 | } | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 673 |  | 
|  | 674 | // Update the payload state with the current response. The payload state | 
|  | 675 | // will automatically reset all stale state if this response is different | 
|  | 676 | // from what's stored already. | 
|  | 677 | PayloadState* payload_state = system_state_->payload_state(); | 
|  | 678 | payload_state->SetResponse(output_object); | 
| rspangler@google.com | 49fdf18 | 2009-10-10 00:57:34 +0000 | [diff] [blame] | 679 | } | 
|  | 680 |  | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 681 | bool OmahaRequestAction::ShouldDeferDownload(OmahaResponse* output_object) { | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 682 | // We should defer the downloads only if we've first satisfied the | 
|  | 683 | // wall-clock-based-waiting period and then the update-check-based waiting | 
|  | 684 | // period, if required. | 
|  | 685 |  | 
|  | 686 | if (!params_->wall_clock_based_wait_enabled) { | 
|  | 687 | // Wall-clock-based waiting period is not enabled, so no scattering needed. | 
|  | 688 | return false; | 
|  | 689 | } | 
|  | 690 |  | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 691 | switch (IsWallClockBasedWaitingSatisfied(output_object)) { | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 692 | case kWallClockWaitNotSatisfied: | 
|  | 693 | // We haven't even satisfied the first condition, passing the | 
|  | 694 | // wall-clock-based waiting period, so we should defer the downloads | 
|  | 695 | // until that happens. | 
|  | 696 | LOG(INFO) << "wall-clock-based-wait not satisfied."; | 
|  | 697 | return true; | 
|  | 698 |  | 
|  | 699 | case kWallClockWaitDoneButUpdateCheckWaitRequired: | 
|  | 700 | LOG(INFO) << "wall-clock-based-wait satisfied and " | 
|  | 701 | << "update-check-based-wait required."; | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 702 | return !IsUpdateCheckCountBasedWaitingSatisfied(); | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 703 |  | 
|  | 704 | case kWallClockWaitDoneAndUpdateCheckWaitNotRequired: | 
|  | 705 | // Wall-clock-based waiting period is satisfied, and it's determined | 
|  | 706 | // that we do not need the update-check-based wait. so no need to | 
|  | 707 | // defer downloads. | 
|  | 708 | LOG(INFO) << "wall-clock-based-wait satisfied and " | 
|  | 709 | << "update-check-based-wait is not required."; | 
|  | 710 | return false; | 
|  | 711 |  | 
|  | 712 | default: | 
|  | 713 | // Returning false for this default case so we err on the | 
|  | 714 | // side of downloading updates than deferring in case of any bugs. | 
|  | 715 | NOTREACHED(); | 
|  | 716 | return false; | 
|  | 717 | } | 
|  | 718 | } | 
|  | 719 |  | 
|  | 720 | OmahaRequestAction::WallClockWaitResult | 
|  | 721 | OmahaRequestAction::IsWallClockBasedWaitingSatisfied( | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 722 | OmahaResponse* output_object) { | 
| Jay Srinivasan | 34b5d86 | 2012-07-23 11:43:22 -0700 | [diff] [blame] | 723 | Time update_first_seen_at; | 
|  | 724 | int64 update_first_seen_at_int; | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 725 |  | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 726 | if (system_state_->prefs()->Exists(kPrefsUpdateFirstSeenAt)) { | 
|  | 727 | if (system_state_->prefs()->GetInt64(kPrefsUpdateFirstSeenAt, | 
|  | 728 | &update_first_seen_at_int)) { | 
| Jay Srinivasan | 34b5d86 | 2012-07-23 11:43:22 -0700 | [diff] [blame] | 729 | // Note: This timestamp could be that of ANY update we saw in the past | 
|  | 730 | // (not necessarily this particular update we're considering to apply) | 
|  | 731 | // but never got to apply because of some reason (e.g. stop AU policy, | 
|  | 732 | // updates being pulled out from Omaha, changes in target version prefix, | 
|  | 733 | // new update being rolled out, etc.). But for the purposes of scattering | 
|  | 734 | // it doesn't matter which update the timestamp corresponds to. i.e. | 
|  | 735 | // the clock starts ticking the first time we see an update and we're | 
|  | 736 | // ready to apply when the random wait period is satisfied relative to | 
|  | 737 | // that first seen timestamp. | 
|  | 738 | update_first_seen_at = Time::FromInternalValue(update_first_seen_at_int); | 
|  | 739 | LOG(INFO) << "Using persisted value of UpdateFirstSeenAt: " | 
|  | 740 | << utils::ToString(update_first_seen_at); | 
|  | 741 | } else { | 
|  | 742 | // This seems like an unexpected error where the persisted value exists | 
|  | 743 | // but it's not readable for some reason. Just skip scattering in this | 
|  | 744 | // case to be safe. | 
|  | 745 | LOG(INFO) << "Not scattering as UpdateFirstSeenAt value cannot be read"; | 
|  | 746 | return kWallClockWaitDoneAndUpdateCheckWaitNotRequired; | 
|  | 747 | } | 
|  | 748 | } else { | 
|  | 749 | update_first_seen_at = Time::Now(); | 
|  | 750 | update_first_seen_at_int = update_first_seen_at.ToInternalValue(); | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 751 | if (system_state_->prefs()->SetInt64(kPrefsUpdateFirstSeenAt, | 
|  | 752 | update_first_seen_at_int)) { | 
| Jay Srinivasan | 34b5d86 | 2012-07-23 11:43:22 -0700 | [diff] [blame] | 753 | LOG(INFO) << "Persisted the new value for UpdateFirstSeenAt: " | 
|  | 754 | << utils::ToString(update_first_seen_at); | 
|  | 755 | } | 
|  | 756 | else { | 
|  | 757 | // This seems like an unexpected error where the value cannot be | 
|  | 758 | // persisted for some reason. Just skip scattering in this | 
|  | 759 | // case to be safe. | 
|  | 760 | LOG(INFO) << "Not scattering as UpdateFirstSeenAt value " | 
|  | 761 | << utils::ToString(update_first_seen_at) | 
|  | 762 | << " cannot be persisted"; | 
|  | 763 | return kWallClockWaitDoneAndUpdateCheckWaitNotRequired; | 
|  | 764 | } | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 765 | } | 
|  | 766 |  | 
| Jay Srinivasan | 34b5d86 | 2012-07-23 11:43:22 -0700 | [diff] [blame] | 767 | TimeDelta elapsed_time = Time::Now() - update_first_seen_at; | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 768 | TimeDelta max_scatter_period = TimeDelta::FromDays( | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 769 | output_object->max_days_to_scatter); | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 770 |  | 
| Jay Srinivasan | 34b5d86 | 2012-07-23 11:43:22 -0700 | [diff] [blame] | 771 | LOG(INFO) << "Waiting Period = " | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 772 | << utils::FormatSecs(params_->waiting_period.InSeconds()) | 
|  | 773 | << ", Time Elapsed = " | 
|  | 774 | << utils::FormatSecs(elapsed_time.InSeconds()) | 
|  | 775 | << ", MaxDaysToScatter = " | 
|  | 776 | << max_scatter_period.InDays(); | 
|  | 777 |  | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 778 | if (!output_object->deadline.empty()) { | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 779 | // The deadline is set for all rules which serve a delta update from a | 
|  | 780 | // previous FSI, which means this update will be applied mostly in OOBE | 
|  | 781 | // cases. For these cases, we shouldn't scatter so as to finish the OOBE | 
|  | 782 | // quickly. | 
|  | 783 | LOG(INFO) << "Not scattering as deadline flag is set"; | 
|  | 784 | return kWallClockWaitDoneAndUpdateCheckWaitNotRequired; | 
|  | 785 | } | 
|  | 786 |  | 
|  | 787 | if (max_scatter_period.InDays() == 0) { | 
|  | 788 | // This means the Omaha rule creator decides that this rule | 
|  | 789 | // should not be scattered irrespective of the policy. | 
|  | 790 | LOG(INFO) << "Not scattering as MaxDaysToScatter in rule is 0."; | 
|  | 791 | return kWallClockWaitDoneAndUpdateCheckWaitNotRequired; | 
|  | 792 | } | 
|  | 793 |  | 
|  | 794 | if (elapsed_time > max_scatter_period) { | 
| Jay Srinivasan | 34b5d86 | 2012-07-23 11:43:22 -0700 | [diff] [blame] | 795 | // This means we've waited more than the upperbound wait in the rule | 
|  | 796 | // from the time we first saw a valid update available to us. | 
|  | 797 | // This will prevent update starvation. | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 798 | LOG(INFO) << "Not scattering as we're past the MaxDaysToScatter limit."; | 
|  | 799 | return kWallClockWaitDoneAndUpdateCheckWaitNotRequired; | 
|  | 800 | } | 
|  | 801 |  | 
|  | 802 | // This means we are required to participate in scattering. | 
|  | 803 | // See if our turn has arrived now. | 
|  | 804 | TimeDelta remaining_wait_time = params_->waiting_period - elapsed_time; | 
|  | 805 | if (remaining_wait_time.InSeconds() <= 0) { | 
|  | 806 | // Yes, it's our turn now. | 
|  | 807 | LOG(INFO) << "Successfully passed the wall-clock-based-wait."; | 
|  | 808 |  | 
|  | 809 | // But we can't download until the update-check-count-based wait is also | 
|  | 810 | // satisfied, so mark it as required now if update checks are enabled. | 
|  | 811 | return params_->update_check_count_wait_enabled ? | 
|  | 812 | kWallClockWaitDoneButUpdateCheckWaitRequired : | 
|  | 813 | kWallClockWaitDoneAndUpdateCheckWaitNotRequired; | 
|  | 814 | } | 
|  | 815 |  | 
|  | 816 | // Not our turn yet, so we have to wait until our turn to | 
|  | 817 | // help scatter the downloads across all clients of the enterprise. | 
|  | 818 | LOG(INFO) << "Update deferred for another " | 
|  | 819 | << utils::FormatSecs(remaining_wait_time.InSeconds()) | 
|  | 820 | << " per policy."; | 
|  | 821 | return kWallClockWaitNotSatisfied; | 
|  | 822 | } | 
|  | 823 |  | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 824 | bool OmahaRequestAction::IsUpdateCheckCountBasedWaitingSatisfied() { | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 825 | int64 update_check_count_value; | 
|  | 826 |  | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 827 | if (system_state_->prefs()->Exists(kPrefsUpdateCheckCount)) { | 
|  | 828 | if (!system_state_->prefs()->GetInt64(kPrefsUpdateCheckCount, | 
|  | 829 | &update_check_count_value)) { | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 830 | // We are unable to read the update check count from file for some reason. | 
|  | 831 | // So let's proceed anyway so as to not stall the update. | 
|  | 832 | LOG(ERROR) << "Unable to read update check count. " | 
|  | 833 | << "Skipping update-check-count-based-wait."; | 
|  | 834 | return true; | 
|  | 835 | } | 
|  | 836 | } else { | 
|  | 837 | // This file does not exist. This means we haven't started our update | 
|  | 838 | // check count down yet, so this is the right time to start the count down. | 
|  | 839 | update_check_count_value = base::RandInt( | 
|  | 840 | params_->min_update_checks_needed, | 
|  | 841 | params_->max_update_checks_allowed); | 
|  | 842 |  | 
|  | 843 | LOG(INFO) << "Randomly picked update check count value = " | 
|  | 844 | << update_check_count_value; | 
|  | 845 |  | 
|  | 846 | // Write out the initial value of update_check_count_value. | 
| Jay Srinivasan | 6f6ea00 | 2012-12-14 11:26:28 -0800 | [diff] [blame^] | 847 | if (!system_state_->prefs()->SetInt64(kPrefsUpdateCheckCount, | 
|  | 848 | update_check_count_value)) { | 
| Jay Srinivasan | 480ddfa | 2012-06-01 19:15:26 -0700 | [diff] [blame] | 849 | // We weren't able to write the update check count file for some reason. | 
|  | 850 | // So let's proceed anyway so as to not stall the update. | 
|  | 851 | LOG(ERROR) << "Unable to write update check count. " | 
|  | 852 | << "Skipping update-check-count-based-wait."; | 
|  | 853 | return true; | 
|  | 854 | } | 
|  | 855 | } | 
|  | 856 |  | 
|  | 857 | if (update_check_count_value == 0) { | 
|  | 858 | LOG(INFO) << "Successfully passed the update-check-based-wait."; | 
|  | 859 | return true; | 
|  | 860 | } | 
|  | 861 |  | 
|  | 862 | if (update_check_count_value < 0 || | 
|  | 863 | update_check_count_value > params_->max_update_checks_allowed) { | 
|  | 864 | // We err on the side of skipping scattering logic instead of stalling | 
|  | 865 | // a machine from receiving any updates in case of any unexpected state. | 
|  | 866 | LOG(ERROR) << "Invalid value for update check count detected. " | 
|  | 867 | << "Skipping update-check-count-based-wait."; | 
|  | 868 | return true; | 
|  | 869 | } | 
|  | 870 |  | 
|  | 871 | // Legal value, we need to wait for more update checks to happen | 
|  | 872 | // until this becomes 0. | 
|  | 873 | LOG(INFO) << "Deferring Omaha updates for another " | 
|  | 874 | << update_check_count_value | 
|  | 875 | << " update checks per policy"; | 
|  | 876 | return false; | 
|  | 877 | } | 
|  | 878 |  | 
|  | 879 | }  // namespace chromeos_update_engine | 
| Jay Srinivasan | 23b92a5 | 2012-10-27 02:00:21 -0700 | [diff] [blame] | 880 |  | 
|  | 881 |  |