michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.background.healthreport; michael@0: michael@0: import java.io.UnsupportedEncodingException; michael@0: import java.security.NoSuchAlgorithmException; michael@0: import java.util.Iterator; michael@0: import java.util.SortedSet; michael@0: michael@0: import org.json.JSONException; michael@0: import org.json.JSONObject; michael@0: import org.mozilla.apache.commons.codec.binary.Base64; michael@0: import org.mozilla.gecko.background.common.log.Logger; michael@0: import org.mozilla.gecko.background.nativecode.NativeCrypto; michael@0: michael@0: public abstract class EnvironmentV1 { michael@0: private static final String LOG_TAG = "GeckoEnvironment"; michael@0: private static final int VERSION = 1; michael@0: michael@0: protected final Class appenderClass; michael@0: michael@0: protected volatile String hash = null; michael@0: protected volatile int id = -1; michael@0: michael@0: public int version = VERSION; michael@0: michael@0: // org.mozilla.profile.age. michael@0: public int profileCreation; michael@0: michael@0: // org.mozilla.sysinfo.sysinfo. michael@0: public int cpuCount; michael@0: public int memoryMB; michael@0: public String architecture; michael@0: public String sysName; michael@0: public String sysVersion; // Kernel. michael@0: michael@0: // geckoAppInfo. michael@0: public String vendor; michael@0: public String appName; michael@0: public String appID; michael@0: public String appVersion; michael@0: public String appBuildID; michael@0: public String platformVersion; michael@0: public String platformBuildID; michael@0: public String os; michael@0: public String xpcomabi; michael@0: public String updateChannel; michael@0: michael@0: // appinfo. michael@0: public int isBlocklistEnabled; michael@0: public int isTelemetryEnabled; michael@0: michael@0: // org.mozilla.addons.active. michael@0: public JSONObject addons = null; michael@0: michael@0: // org.mozilla.addons.counts. michael@0: public int extensionCount; michael@0: public int pluginCount; michael@0: public int themeCount; michael@0: michael@0: /** michael@0: * We break out this interface in order to allow for testing -- pass in your michael@0: * own appender that just records strings, for example. michael@0: */ michael@0: public static abstract class EnvironmentAppender { michael@0: public abstract void append(String s); michael@0: public abstract void append(int v); michael@0: } michael@0: michael@0: public static class HashAppender extends EnvironmentAppender { michael@0: private final StringBuilder builder; michael@0: michael@0: public HashAppender() throws NoSuchAlgorithmException { michael@0: builder = new StringBuilder(); michael@0: } michael@0: michael@0: @Override michael@0: public void append(String s) { michael@0: builder.append((s == null) ? "null" : s); michael@0: } michael@0: michael@0: @Override michael@0: public void append(int profileCreation) { michael@0: append(Integer.toString(profileCreation, 10)); michael@0: } michael@0: michael@0: @Override michael@0: public String toString() { michael@0: // We *could* use ASCII85… but the savings would be negated by the michael@0: // inclusion of JSON-unsafe characters like double-quote. michael@0: final byte[] inputBytes; michael@0: try { michael@0: inputBytes = builder.toString().getBytes("UTF-8"); michael@0: } catch (UnsupportedEncodingException e) { michael@0: Logger.warn(LOG_TAG, "Invalid charset String passed to getBytes", e); michael@0: return null; michael@0: } michael@0: michael@0: // Note to the security-minded reader: we deliberately use SHA-1 here, not michael@0: // a stronger hash. These identifiers don't strictly need a cryptographic michael@0: // hash function, because there is negligible value in attacking the hash. michael@0: // We use SHA-1 because it's *shorter* -- the exact same reason that Git michael@0: // chose SHA-1. michael@0: final byte[] hash = NativeCrypto.sha1(inputBytes); michael@0: return new Base64(-1, null, false).encodeAsString(hash); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Ensure that the {@link Environment} has been registered with its michael@0: * storage layer, and can be used to annotate events. michael@0: * michael@0: * It's safe to call this method more than once, and each time you'll michael@0: * get the same ID. michael@0: * michael@0: * @return the integer ID to use in subsequent DB insertions. michael@0: */ michael@0: public abstract int register(); michael@0: michael@0: protected EnvironmentAppender getAppender() { michael@0: EnvironmentAppender appender = null; michael@0: try { michael@0: appender = appenderClass.newInstance(); michael@0: } catch (InstantiationException ex) { michael@0: // Should never happen, but... michael@0: Logger.warn(LOG_TAG, "Could not compute hash.", ex); michael@0: } catch (IllegalAccessException ex) { michael@0: // Should never happen, but... michael@0: Logger.warn(LOG_TAG, "Could not compute hash.", ex); michael@0: } michael@0: return appender; michael@0: } michael@0: michael@0: protected void appendHash(EnvironmentAppender appender) { michael@0: appender.append(profileCreation); michael@0: appender.append(cpuCount); michael@0: appender.append(memoryMB); michael@0: appender.append(architecture); michael@0: appender.append(sysName); michael@0: appender.append(sysVersion); michael@0: appender.append(vendor); michael@0: appender.append(appName); michael@0: appender.append(appID); michael@0: appender.append(appVersion); michael@0: appender.append(appBuildID); michael@0: appender.append(platformVersion); michael@0: appender.append(platformBuildID); michael@0: appender.append(os); michael@0: appender.append(xpcomabi); michael@0: appender.append(updateChannel); michael@0: appender.append(isBlocklistEnabled); michael@0: appender.append(isTelemetryEnabled); michael@0: appender.append(extensionCount); michael@0: appender.append(pluginCount); michael@0: appender.append(themeCount); michael@0: michael@0: // We need sorted values. michael@0: if (addons != null) { michael@0: appendSortedAddons(getNonIgnoredAddons(), appender); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Compute the stable hash of the configured environment. michael@0: * michael@0: * @return the hash in base34, or null if there was a problem. michael@0: */ michael@0: public String getHash() { michael@0: // It's never unset, so we only care about partial reads. volatile is enough. michael@0: if (hash != null) { michael@0: return hash; michael@0: } michael@0: michael@0: EnvironmentAppender appender = getAppender(); michael@0: if (appender == null) { michael@0: return null; michael@0: } michael@0: michael@0: appendHash(appender); michael@0: return hash = appender.toString(); michael@0: } michael@0: michael@0: public EnvironmentV1(Class appenderClass) { michael@0: super(); michael@0: this.appenderClass = appenderClass; michael@0: } michael@0: michael@0: public JSONObject getNonIgnoredAddons() { michael@0: if (addons == null) { michael@0: return null; michael@0: } michael@0: JSONObject out = new JSONObject(); michael@0: @SuppressWarnings("unchecked") michael@0: Iterator keys = addons.keys(); michael@0: while (keys.hasNext()) { michael@0: try { michael@0: final String key = keys.next(); michael@0: final Object obj = addons.get(key); michael@0: if (obj != null && michael@0: obj instanceof JSONObject && michael@0: ((JSONObject) obj).optBoolean("ignore", false)) { michael@0: continue; michael@0: } michael@0: out.put(key, obj); michael@0: } catch (JSONException ex) { michael@0: // Do nothing. michael@0: } michael@0: } michael@0: return out; michael@0: } michael@0: michael@0: /** michael@0: * Take a collection of add-on descriptors, appending a consistent string michael@0: * to the provided builder. michael@0: */ michael@0: public static void appendSortedAddons(JSONObject addons, final EnvironmentAppender builder) { michael@0: final SortedSet keys = HealthReportUtils.sortedKeySet(addons); michael@0: michael@0: // For each add-on, produce a consistent, sorted mapping of its descriptor. michael@0: for (String key : keys) { michael@0: try { michael@0: JSONObject addon = addons.getJSONObject(key); michael@0: michael@0: // Now produce the output for this add-on. michael@0: builder.append(key); michael@0: builder.append("={"); michael@0: michael@0: for (String addonKey : HealthReportUtils.sortedKeySet(addon)) { michael@0: builder.append(addonKey); michael@0: builder.append("=="); michael@0: try { michael@0: builder.append(addon.get(addonKey).toString()); michael@0: } catch (JSONException e) { michael@0: builder.append("_e_"); michael@0: } michael@0: } michael@0: michael@0: builder.append("}"); michael@0: } catch (Exception e) { michael@0: // Muffle. michael@0: Logger.warn(LOG_TAG, "Invalid add-on for ID " + key); michael@0: } michael@0: } michael@0: } michael@0: michael@0: public void setJSONForAddons(byte[] json) throws Exception { michael@0: setJSONForAddons(new String(json, "UTF-8")); michael@0: } michael@0: michael@0: public void setJSONForAddons(String json) throws Exception { michael@0: if (json == null || "null".equals(json)) { michael@0: addons = null; michael@0: return; michael@0: } michael@0: addons = new JSONObject(json); michael@0: } michael@0: michael@0: public void setJSONForAddons(JSONObject json) { michael@0: addons = json; michael@0: } michael@0: michael@0: /** michael@0: * Includes ignored add-ons. michael@0: */ michael@0: public String getNormalizedAddonsJSON() { michael@0: // We trust that our input will already be normalized. If that assumption michael@0: // is invalidated, then we'll be sorry. michael@0: return (addons == null) ? "null" : addons.toString(); michael@0: } michael@0: }