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