1 // Copyright 2014 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.omaha; 6 7 import android.text.TextUtils; 8 import android.util.Log; 9 10 import org.chromium.chrome.browser.omaha.OmahaBase.VersionConfig; 11 import org.chromium.chrome.browser.omaha.XMLParser.Node; 12 13 /** 14 * Parses XML responses from the Omaha Update Server. 15 * 16 * Expects XML formatted like: 17 * <?xml version="1.0" encoding="UTF-8"?> 18 * <daystart elapsed_days="4804" elapsed_seconds="65524"/> 19 * <app appid="{appid}" status="ok"> 20 * <updatecheck status="ok"> 21 * <urls> 22 * <url codebase="https://market.android.com/details?id=com.google.android.apps.chrome/"/> 23 * </urls> 24 * <manifest version="0.16.4130.199"> 25 * <packages> 26 * <package hash="0" name="dummy.apk" required="true" size="0"/> 27 * </packages> 28 * <actions> 29 * <action event="install" run="dummy.apk"/> 30 * <action event="postinstall"/> 31 * </actions> 32 * </manifest> 33 * </updatecheck> 34 * <ping status="ok"/> 35 * </app> 36 * </response> 37 * 38 * The appid is dependent on the variant of Chrome that is running. 39 */ 40 public class ResponseParser { 41 private static final String TAG = "ResponseParser"; 42 43 // Tags that we care to parse from the response. 44 private static final String TAG_APP = "app"; 45 private static final String TAG_DAYSTART = "daystart"; 46 private static final String TAG_EVENT = "event"; 47 private static final String TAG_MANIFEST = "manifest"; 48 private static final String TAG_PING = "ping"; 49 private static final String TAG_RESPONSE = "response"; 50 private static final String TAG_UPDATECHECK = "updatecheck"; 51 private static final String TAG_URL = "url"; 52 private static final String TAG_URLS = "urls"; 53 54 private final String mAppId; 55 private final boolean mExpectInstallEvent; 56 private final boolean mExpectPing; 57 private final boolean mExpectUpdatecheck; 58 private final boolean mStrictParsingMode; 59 60 private Integer mDaystartSeconds; 61 private Integer mDaystartDays; 62 private String mAppStatus; 63 64 private String mUpdateStatus; 65 private String mNewVersion; 66 private String mUrl; 67 68 private boolean mParsedInstallEvent; 69 private boolean mParsedPing; 70 private boolean mParsedUpdatecheck; 71 ResponseParser(String appId, boolean expectInstallEvent)72 public ResponseParser(String appId, boolean expectInstallEvent) { 73 this(appId, expectInstallEvent, !expectInstallEvent, !expectInstallEvent); 74 } 75 ResponseParser(String appId, boolean expectInstallEvent, boolean expectPing, boolean expectUpdatecheck)76 public ResponseParser(String appId, boolean expectInstallEvent, boolean expectPing, 77 boolean expectUpdatecheck) { 78 this(false, appId, expectInstallEvent, expectPing, expectUpdatecheck); 79 } 80 ResponseParser(boolean strictParsing, String appId, boolean expectInstallEvent, boolean expectPing, boolean expectUpdatecheck)81 public ResponseParser(boolean strictParsing, String appId, boolean expectInstallEvent, 82 boolean expectPing, boolean expectUpdatecheck) { 83 mStrictParsingMode = strictParsing; 84 mAppId = appId; 85 mExpectInstallEvent = expectInstallEvent; 86 mExpectPing = expectPing; 87 mExpectUpdatecheck = expectUpdatecheck; 88 } 89 parseResponse(String xml)90 public VersionConfig parseResponse(String xml) throws RequestFailureException { 91 XMLParser parser = new XMLParser(xml); 92 Node rootNode = parser.getRootNode(); 93 parseRootNode(rootNode); 94 return new VersionConfig(getNewVersion(), getURL(), getDaystartDays(), getUpdateStatus()); 95 } 96 getDaystartSeconds()97 public int getDaystartSeconds() { 98 if (mDaystartSeconds == null) return 0; 99 return mDaystartSeconds; 100 } 101 getDaystartDays()102 public int getDaystartDays() { 103 if (mDaystartDays == null) return 0; 104 return mDaystartDays; 105 } 106 getNewVersion()107 public String getNewVersion() { 108 return mNewVersion; 109 } 110 getURL()111 public String getURL() { 112 return mUrl; 113 } 114 getAppStatus()115 public String getAppStatus() { 116 return mAppStatus; 117 } 118 getUpdateStatus()119 public String getUpdateStatus() { 120 return mUpdateStatus; 121 } 122 resetParsedData()123 private void resetParsedData() { 124 mDaystartSeconds = null; 125 mNewVersion = null; 126 mUrl = null; 127 mUpdateStatus = null; 128 mAppStatus = null; 129 130 mParsedInstallEvent = false; 131 mParsedPing = false; 132 mParsedUpdatecheck = false; 133 } 134 logError(Node node, int errorCode)135 private boolean logError(Node node, int errorCode) throws RequestFailureException { 136 String errorMessage = "Failed to parse: " + node.tag; 137 if (mStrictParsingMode) throw new RequestFailureException(errorMessage, errorCode); 138 139 Log.e(TAG, errorMessage); 140 return false; 141 } 142 parseRootNode(Node rootNode)143 private void parseRootNode(Node rootNode) throws RequestFailureException { 144 for (int i = 0; i < rootNode.children.size(); ++i) { 145 if (TextUtils.equals(TAG_RESPONSE, rootNode.children.get(i).tag)) { 146 if (parseResponseNode(rootNode.children.get(i))) return; 147 break; 148 } 149 } 150 151 // The tag was bad; reset all of our state and bail. 152 resetParsedData(); 153 logError(rootNode, RequestFailureException.ERROR_PARSE_ROOT); 154 } 155 parseResponseNode(Node node)156 private boolean parseResponseNode(Node node) throws RequestFailureException { 157 boolean success = true; 158 String serverType = node.attributes.get("server"); 159 success &= TextUtils.equals("3.0", node.attributes.get("protocol")); 160 161 if (!TextUtils.equals("prod", serverType)) Log.w(TAG, "Server type: " + serverType); 162 163 for (int i = 0; i < node.children.size(); ++i) { 164 Node current = node.children.get(i); 165 if (TextUtils.equals(TAG_DAYSTART, current.tag)) { 166 success &= parseDaystartNode(current); 167 } else if (TextUtils.equals(TAG_APP, current.tag)) { 168 success &= parseAppNode(current); 169 } else { 170 Log.w(TAG, "Ignoring unknown child of <" + node.tag + "> : " + current.tag); 171 } 172 } 173 174 if (!success) { 175 return logError(node, RequestFailureException.ERROR_PARSE_RESPONSE); 176 } else if (mDaystartSeconds == null) { 177 return logError(node, RequestFailureException.ERROR_PARSE_DAYSTART); 178 } else if (mAppStatus == null) { 179 return logError(node, RequestFailureException.ERROR_PARSE_APP); 180 } else if (mExpectInstallEvent != mParsedInstallEvent) { 181 return logError(node, RequestFailureException.ERROR_PARSE_EVENT); 182 } else if (mExpectPing != mParsedPing) { 183 return logError(node, RequestFailureException.ERROR_PARSE_PING); 184 } else if (mExpectUpdatecheck != mParsedUpdatecheck) { 185 return logError(node, RequestFailureException.ERROR_PARSE_UPDATECHECK); 186 } 187 188 return true; 189 } 190 parseDaystartNode(Node node)191 private boolean parseDaystartNode(Node node) throws RequestFailureException { 192 try { 193 mDaystartSeconds = Integer.parseInt(node.attributes.get("elapsed_seconds")); 194 mDaystartDays = Integer.parseInt(node.attributes.get("elapsed_days")); 195 } catch (NumberFormatException e) { 196 return logError(node, RequestFailureException.ERROR_PARSE_DAYSTART); 197 } 198 return true; 199 } 200 parseAppNode(Node node)201 private boolean parseAppNode(Node node) throws RequestFailureException { 202 boolean success = true; 203 success &= TextUtils.equals(mAppId, node.attributes.get("appid")); 204 205 mAppStatus = node.attributes.get("status"); 206 if (TextUtils.equals("ok", mAppStatus)) { 207 for (int i = 0; i < node.children.size(); ++i) { 208 Node current = node.children.get(i); 209 if (TextUtils.equals(TAG_UPDATECHECK, current.tag)) { 210 success &= parseUpdatecheck(current); 211 } else if (TextUtils.equals(TAG_EVENT, current.tag)) { 212 parseEvent(current); 213 } else if (TextUtils.equals(TAG_PING, current.tag)) { 214 parsePing(current); 215 } 216 } 217 } else if (TextUtils.equals("restricted", mAppStatus)) { 218 // Omaha isn't allowed to get data in this country. Pretend the request was fine. 219 } else { 220 success = false; 221 } 222 223 if (success) return true; 224 return logError(node, RequestFailureException.ERROR_PARSE_APP); 225 } 226 parseUpdatecheck(Node node)227 private boolean parseUpdatecheck(Node node) throws RequestFailureException { 228 boolean success = true; 229 230 mUpdateStatus = node.attributes.get("status"); 231 if (TextUtils.equals("ok", mUpdateStatus)) { 232 for (int i = 0; i < node.children.size(); ++i) { 233 Node current = node.children.get(i); 234 if (TextUtils.equals(TAG_URLS, current.tag)) { 235 parseUrls(current); 236 } else if (TextUtils.equals(TAG_MANIFEST, current.tag)) { 237 parseManifest(current); 238 } 239 } 240 241 // Confirm all the tags we expected to see were parsed properly. 242 if (mUrl == null) { 243 return logError(node, RequestFailureException.ERROR_PARSE_URLS); 244 } else if (mNewVersion == null) { 245 return logError(node, RequestFailureException.ERROR_PARSE_MANIFEST); 246 } 247 } else if (TextUtils.equals("noupdate", mUpdateStatus)) { 248 // No update is available. Don't bother searching for other attributes. 249 } else if (mUpdateStatus != null && mUpdateStatus.startsWith("error")) { 250 Log.w(TAG, "Ignoring error status for " + node.tag + ": " + mUpdateStatus); 251 } else { 252 Log.w(TAG, "Ignoring unknown status for " + node.tag + ": " + mUpdateStatus); 253 } 254 255 mParsedUpdatecheck = true; 256 return true; 257 } 258 parsePing(Node node)259 private void parsePing(Node node) { 260 if (TextUtils.equals("ok", node.attributes.get("status"))) mParsedPing = true; 261 } 262 parseEvent(Node node)263 private void parseEvent(Node node) { 264 if (TextUtils.equals("ok", node.attributes.get("status"))) mParsedInstallEvent = true; 265 } 266 parseUrls(Node node)267 private void parseUrls(Node node) { 268 for (int i = 0; i < node.children.size(); ++i) { 269 Node current = node.children.get(i); 270 if (TextUtils.equals(TAG_URL, current.tag)) parseUrl(current); 271 } 272 } 273 parseUrl(Node node)274 private void parseUrl(Node node) { 275 String url = node.attributes.get("codebase"); 276 if (url == null) return; 277 278 // The URL gets a "/" tacked onto it by the server. Remove it. 279 if (url.endsWith("/")) url = url.substring(0, url.length() - 1); 280 mUrl = url; 281 } 282 parseManifest(Node node)283 private void parseManifest(Node node) { 284 mNewVersion = node.attributes.get("version"); 285 } 286 } 287