|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 package org.mozilla.gecko.background.healthreport; |
|
6 |
|
7 import java.io.UnsupportedEncodingException; |
|
8 import java.security.NoSuchAlgorithmException; |
|
9 import java.util.Iterator; |
|
10 import java.util.SortedSet; |
|
11 |
|
12 import org.json.JSONException; |
|
13 import org.json.JSONObject; |
|
14 import org.mozilla.apache.commons.codec.binary.Base64; |
|
15 import org.mozilla.gecko.background.common.log.Logger; |
|
16 import org.mozilla.gecko.background.nativecode.NativeCrypto; |
|
17 |
|
18 public abstract class EnvironmentV1 { |
|
19 private static final String LOG_TAG = "GeckoEnvironment"; |
|
20 private static final int VERSION = 1; |
|
21 |
|
22 protected final Class<? extends EnvironmentAppender> appenderClass; |
|
23 |
|
24 protected volatile String hash = null; |
|
25 protected volatile int id = -1; |
|
26 |
|
27 public int version = VERSION; |
|
28 |
|
29 // org.mozilla.profile.age. |
|
30 public int profileCreation; |
|
31 |
|
32 // org.mozilla.sysinfo.sysinfo. |
|
33 public int cpuCount; |
|
34 public int memoryMB; |
|
35 public String architecture; |
|
36 public String sysName; |
|
37 public String sysVersion; // Kernel. |
|
38 |
|
39 // geckoAppInfo. |
|
40 public String vendor; |
|
41 public String appName; |
|
42 public String appID; |
|
43 public String appVersion; |
|
44 public String appBuildID; |
|
45 public String platformVersion; |
|
46 public String platformBuildID; |
|
47 public String os; |
|
48 public String xpcomabi; |
|
49 public String updateChannel; |
|
50 |
|
51 // appinfo. |
|
52 public int isBlocklistEnabled; |
|
53 public int isTelemetryEnabled; |
|
54 |
|
55 // org.mozilla.addons.active. |
|
56 public JSONObject addons = null; |
|
57 |
|
58 // org.mozilla.addons.counts. |
|
59 public int extensionCount; |
|
60 public int pluginCount; |
|
61 public int themeCount; |
|
62 |
|
63 /** |
|
64 * We break out this interface in order to allow for testing -- pass in your |
|
65 * own appender that just records strings, for example. |
|
66 */ |
|
67 public static abstract class EnvironmentAppender { |
|
68 public abstract void append(String s); |
|
69 public abstract void append(int v); |
|
70 } |
|
71 |
|
72 public static class HashAppender extends EnvironmentAppender { |
|
73 private final StringBuilder builder; |
|
74 |
|
75 public HashAppender() throws NoSuchAlgorithmException { |
|
76 builder = new StringBuilder(); |
|
77 } |
|
78 |
|
79 @Override |
|
80 public void append(String s) { |
|
81 builder.append((s == null) ? "null" : s); |
|
82 } |
|
83 |
|
84 @Override |
|
85 public void append(int profileCreation) { |
|
86 append(Integer.toString(profileCreation, 10)); |
|
87 } |
|
88 |
|
89 @Override |
|
90 public String toString() { |
|
91 // We *could* use ASCII85… but the savings would be negated by the |
|
92 // inclusion of JSON-unsafe characters like double-quote. |
|
93 final byte[] inputBytes; |
|
94 try { |
|
95 inputBytes = builder.toString().getBytes("UTF-8"); |
|
96 } catch (UnsupportedEncodingException e) { |
|
97 Logger.warn(LOG_TAG, "Invalid charset String passed to getBytes", e); |
|
98 return null; |
|
99 } |
|
100 |
|
101 // Note to the security-minded reader: we deliberately use SHA-1 here, not |
|
102 // a stronger hash. These identifiers don't strictly need a cryptographic |
|
103 // hash function, because there is negligible value in attacking the hash. |
|
104 // We use SHA-1 because it's *shorter* -- the exact same reason that Git |
|
105 // chose SHA-1. |
|
106 final byte[] hash = NativeCrypto.sha1(inputBytes); |
|
107 return new Base64(-1, null, false).encodeAsString(hash); |
|
108 } |
|
109 } |
|
110 |
|
111 /** |
|
112 * Ensure that the {@link Environment} has been registered with its |
|
113 * storage layer, and can be used to annotate events. |
|
114 * |
|
115 * It's safe to call this method more than once, and each time you'll |
|
116 * get the same ID. |
|
117 * |
|
118 * @return the integer ID to use in subsequent DB insertions. |
|
119 */ |
|
120 public abstract int register(); |
|
121 |
|
122 protected EnvironmentAppender getAppender() { |
|
123 EnvironmentAppender appender = null; |
|
124 try { |
|
125 appender = appenderClass.newInstance(); |
|
126 } catch (InstantiationException ex) { |
|
127 // Should never happen, but... |
|
128 Logger.warn(LOG_TAG, "Could not compute hash.", ex); |
|
129 } catch (IllegalAccessException ex) { |
|
130 // Should never happen, but... |
|
131 Logger.warn(LOG_TAG, "Could not compute hash.", ex); |
|
132 } |
|
133 return appender; |
|
134 } |
|
135 |
|
136 protected void appendHash(EnvironmentAppender appender) { |
|
137 appender.append(profileCreation); |
|
138 appender.append(cpuCount); |
|
139 appender.append(memoryMB); |
|
140 appender.append(architecture); |
|
141 appender.append(sysName); |
|
142 appender.append(sysVersion); |
|
143 appender.append(vendor); |
|
144 appender.append(appName); |
|
145 appender.append(appID); |
|
146 appender.append(appVersion); |
|
147 appender.append(appBuildID); |
|
148 appender.append(platformVersion); |
|
149 appender.append(platformBuildID); |
|
150 appender.append(os); |
|
151 appender.append(xpcomabi); |
|
152 appender.append(updateChannel); |
|
153 appender.append(isBlocklistEnabled); |
|
154 appender.append(isTelemetryEnabled); |
|
155 appender.append(extensionCount); |
|
156 appender.append(pluginCount); |
|
157 appender.append(themeCount); |
|
158 |
|
159 // We need sorted values. |
|
160 if (addons != null) { |
|
161 appendSortedAddons(getNonIgnoredAddons(), appender); |
|
162 } |
|
163 } |
|
164 |
|
165 /** |
|
166 * Compute the stable hash of the configured environment. |
|
167 * |
|
168 * @return the hash in base34, or null if there was a problem. |
|
169 */ |
|
170 public String getHash() { |
|
171 // It's never unset, so we only care about partial reads. volatile is enough. |
|
172 if (hash != null) { |
|
173 return hash; |
|
174 } |
|
175 |
|
176 EnvironmentAppender appender = getAppender(); |
|
177 if (appender == null) { |
|
178 return null; |
|
179 } |
|
180 |
|
181 appendHash(appender); |
|
182 return hash = appender.toString(); |
|
183 } |
|
184 |
|
185 public EnvironmentV1(Class<? extends EnvironmentAppender> appenderClass) { |
|
186 super(); |
|
187 this.appenderClass = appenderClass; |
|
188 } |
|
189 |
|
190 public JSONObject getNonIgnoredAddons() { |
|
191 if (addons == null) { |
|
192 return null; |
|
193 } |
|
194 JSONObject out = new JSONObject(); |
|
195 @SuppressWarnings("unchecked") |
|
196 Iterator<String> keys = addons.keys(); |
|
197 while (keys.hasNext()) { |
|
198 try { |
|
199 final String key = keys.next(); |
|
200 final Object obj = addons.get(key); |
|
201 if (obj != null && |
|
202 obj instanceof JSONObject && |
|
203 ((JSONObject) obj).optBoolean("ignore", false)) { |
|
204 continue; |
|
205 } |
|
206 out.put(key, obj); |
|
207 } catch (JSONException ex) { |
|
208 // Do nothing. |
|
209 } |
|
210 } |
|
211 return out; |
|
212 } |
|
213 |
|
214 /** |
|
215 * Take a collection of add-on descriptors, appending a consistent string |
|
216 * to the provided builder. |
|
217 */ |
|
218 public static void appendSortedAddons(JSONObject addons, final EnvironmentAppender builder) { |
|
219 final SortedSet<String> keys = HealthReportUtils.sortedKeySet(addons); |
|
220 |
|
221 // For each add-on, produce a consistent, sorted mapping of its descriptor. |
|
222 for (String key : keys) { |
|
223 try { |
|
224 JSONObject addon = addons.getJSONObject(key); |
|
225 |
|
226 // Now produce the output for this add-on. |
|
227 builder.append(key); |
|
228 builder.append("={"); |
|
229 |
|
230 for (String addonKey : HealthReportUtils.sortedKeySet(addon)) { |
|
231 builder.append(addonKey); |
|
232 builder.append("=="); |
|
233 try { |
|
234 builder.append(addon.get(addonKey).toString()); |
|
235 } catch (JSONException e) { |
|
236 builder.append("_e_"); |
|
237 } |
|
238 } |
|
239 |
|
240 builder.append("}"); |
|
241 } catch (Exception e) { |
|
242 // Muffle. |
|
243 Logger.warn(LOG_TAG, "Invalid add-on for ID " + key); |
|
244 } |
|
245 } |
|
246 } |
|
247 |
|
248 public void setJSONForAddons(byte[] json) throws Exception { |
|
249 setJSONForAddons(new String(json, "UTF-8")); |
|
250 } |
|
251 |
|
252 public void setJSONForAddons(String json) throws Exception { |
|
253 if (json == null || "null".equals(json)) { |
|
254 addons = null; |
|
255 return; |
|
256 } |
|
257 addons = new JSONObject(json); |
|
258 } |
|
259 |
|
260 public void setJSONForAddons(JSONObject json) { |
|
261 addons = json; |
|
262 } |
|
263 |
|
264 /** |
|
265 * Includes ignored add-ons. |
|
266 */ |
|
267 public String getNormalizedAddonsJSON() { |
|
268 // We trust that our input will already be normalized. If that assumption |
|
269 // is invalidated, then we'll be sorry. |
|
270 return (addons == null) ? "null" : addons.toString(); |
|
271 } |
|
272 } |