private static void recordFetchTelemetry(final Exception exception) { if (exception == null) { // Should never happen. Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_EXCEPTION); return; } if (exception instanceof UnknownHostException) { // Unknown host => we're offline. Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_OFFLINE); return; } if (exception instanceof SSLException) { Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_SSL_ERROR); return; } if (exception instanceof ProtocolException || exception instanceof SocketException) { Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_SOCKET_ERROR); return; } Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_EXCEPTION); }
/** * Fetch the provided URI, returning a {@link JarInputStream} if the response body is appropriate. * * <p>Protected to allow for mocking. * * @return the entity body as a stream, or null on failure. */ @SuppressWarnings("static-method") @RobocopTarget protected JarInputStream fetchDistribution(URI uri, HttpURLConnection connection) throws IOException { final int status = connection.getResponseCode(); Log.d(LOGTAG, "Distribution fetch: " + status); // We record HTTP statuses as 2xx, 3xx, 4xx, 5xx => 2, 3, 4, 5. final int value; if (status > 599 || status < 100) { Log.wtf(LOGTAG, "Unexpected HTTP status code: " + status); value = CODE_CATEGORY_STATUS_OUT_OF_RANGE; } else { value = status / 100; } Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, value); if (status != 200) { Log.w(LOGTAG, "Got status " + status + " fetching distribution."); Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_NON_SUCCESS_RESPONSE); return null; } final String contentType = connection.getContentType(); if (contentType == null || !contentType.startsWith(EXPECTED_CONTENT_TYPE)) { Log.w(LOGTAG, "Malformed response: invalid Content-Type."); Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_INVALID_CONTENT_TYPE); return null; } return new JarInputStream(new BufferedInputStream(connection.getInputStream()), true); }
/** * Get the Android preferences from the preferences.json file, if any exist. * * @return The preferences in a JSONObject, or an empty JSONObject if no preferences are defined. */ public JSONObject getAndroidPreferences() { final File descFile = getDistributionFile("preferences.json"); if (descFile == null) { // Logging and existence checks are handled in getDistributionFile. return new JSONObject(); } try { final JSONObject all = FileUtils.readJSONObjectFromFile(descFile); if (!all.has("AndroidPreferences")) { return new JSONObject(); } return all.getJSONObject("AndroidPreferences"); } catch (IOException e) { Log.e(LOGTAG, "Error getting distribution descriptor file.", e); Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); return new JSONObject(); } catch (JSONException e) { Log.e(LOGTAG, "Error parsing preferences.json", e); Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); return new JSONObject(); } }
public DistributionDescriptor getDescriptor() { File descFile = getDistributionFile("preferences.json"); if (descFile == null) { // Logging and existence checks are handled in getDistributionFile. return null; } try { JSONObject all = FileUtils.readJSONObjectFromFile(descFile); if (!all.has("Global")) { Log.e(LOGTAG, "Distribution preferences.json has no Global entry!"); return null; } return new DistributionDescriptor(all.getJSONObject("Global")); } catch (IOException e) { Log.e(LOGTAG, "Error getting distribution descriptor file.", e); Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); return null; } catch (JSONException e) { Log.e(LOGTAG, "Error parsing preferences.json", e); Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); return null; } }
public JSONArray getBookmarks() { File bookmarks = getDistributionFile("bookmarks.json"); if (bookmarks == null) { // Logging and existence checks are handled in getDistributionFile. return null; } try { return new JSONArray(FileUtils.readStringFromFile(bookmarks)); } catch (IOException e) { Log.e(LOGTAG, "Error getting bookmarks", e); Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); return null; } catch (JSONException e) { Log.e(LOGTAG, "Error parsing bookmarks.json", e); Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); return null; } }
private URI getReferredDistribution(ReferrerDescriptor descriptor) { final String content = descriptor.content; if (content == null) { return null; } // We restrict here to avoid injection attacks. After all, // we're downloading a distribution payload based on intent input. if (!content.matches("^[a-zA-Z0-9]+$")) { Log.e(LOGTAG, "Invalid referrer content: " + content); Telemetry.addToHistogram(HISTOGRAM_REFERRER_INVALID, 1); return null; } try { return new URI(FETCH_PROTOCOL, FETCH_HOSTNAME, FETCH_PATH + content + FETCH_EXTENSION, null); } catch (URISyntaxException e) { // This should never occur. Log.wtf(LOGTAG, "Invalid URI with content " + content + "!"); return null; } }
/** * If applicable, download and select the distribution specified in the referrer intent. * * @return true if a referrer-supplied distribution was selected. */ private boolean checkIntentDistribution(final ReferrerDescriptor referrer) { if (referrer == null) { return false; } URI uri = getReferredDistribution(referrer); if (uri == null) { return false; } long start = SystemClock.uptimeMillis(); Log.v(LOGTAG, "Downloading referred distribution: " + uri); try { final HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection(); // If the Search Activity starts, and we handle the referrer intent, this'll return // null. Recover gracefully in this case. final GeckoAppShell.GeckoInterface geckoInterface = GeckoAppShell.getGeckoInterface(); final String ua; if (geckoInterface == null) { // Fall back to GeckoApp's default implementation. ua = HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET : AppConstants.USER_AGENT_FENNEC_MOBILE; } else { ua = geckoInterface.getDefaultUAString(); } connection.setRequestProperty(HTTP.USER_AGENT, ua); connection.setRequestProperty("Accept", EXPECTED_CONTENT_TYPE); try { final JarInputStream distro; try { distro = fetchDistribution(uri, connection); } catch (Exception e) { Log.e(LOGTAG, "Error fetching distribution from network.", e); recordFetchTelemetry(e); return false; } long end = SystemClock.uptimeMillis(); final long duration = end - start; Log.d(LOGTAG, "Distro fetch took " + duration + "ms; result? " + (distro != null)); Telemetry.addToHistogram( HISTOGRAM_DOWNLOAD_TIME_MS, clamp(MAX_DOWNLOAD_TIME_MSEC, duration)); if (distro == null) { // Nothing to do. return false; } // Try to copy distribution files from the fetched stream. try { Log.d(LOGTAG, "Copying files from fetched zip."); if (copyFilesFromStream(distro)) { // We always copy to the data dir, and we only copy files from // a 'distribution' subdirectory. Now determine our actual distribution directory. return checkDataDistribution(); } } catch (SecurityException e) { Log.e(LOGTAG, "Security exception copying files. Corrupt or malicious?", e); Telemetry.addToHistogram( HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_POST_FETCH_SECURITY_EXCEPTION); } catch (Exception e) { Log.e(LOGTAG, "Error copying files from distribution.", e); Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_POST_FETCH_EXCEPTION); } finally { distro.close(); } } finally { connection.disconnect(); } } catch (IOException e) { Log.e(LOGTAG, "Error copying distribution files from network.", e); recordFetchTelemetry(e); } return false; }