mobile/android/base/background/datareporting/TelemetryRecorder.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 package org.mozilla.gecko.background.datareporting;
michael@0 6
michael@0 7 import java.io.BufferedOutputStream;
michael@0 8 import java.io.File;
michael@0 9 import java.io.FileOutputStream;
michael@0 10 import java.io.IOException;
michael@0 11 import java.io.OutputStream;
michael@0 12 import java.io.UnsupportedEncodingException;
michael@0 13 import java.security.MessageDigest;
michael@0 14 import java.security.NoSuchAlgorithmException;
michael@0 15
michael@0 16 import org.json.JSONObject;
michael@0 17 import org.mozilla.gecko.background.common.log.Logger;
michael@0 18
michael@0 19 import android.util.Base64;
michael@0 20
michael@0 21 /**
michael@0 22 * Writes telemetry ping to file.
michael@0 23 *
michael@0 24 * Also creates and updates a SHA-256 checksum for the payload to be included in the ping
michael@0 25 * file.
michael@0 26 *
michael@0 27 * A saved telemetry ping file consists of JSON in the following format,
michael@0 28 * {
michael@0 29 * "slug": "<uuid-string>",
michael@0 30 * "payload": "<escaped-json-data-string>",
michael@0 31 * "checksum": "<base64-sha-256-string>"
michael@0 32 * }
michael@0 33 *
michael@0 34 * This class writes first to a temporary file and then, after finishing the contents of the ping,
michael@0 35 * moves that to the file specified by the caller. This is to avoid uploads of partially written
michael@0 36 * ping files.
michael@0 37 *
michael@0 38 * The API provided by this class:
michael@0 39 * startPingFile() - opens stream to a tmp File in the Android cache directory and writes the slug header
michael@0 40 * appendPayload(String payloadContent) - appends to the payload of the ping and updates the checksum
michael@0 41 * finishPingFile() - writes the checksum to the tmp file and moves it to the File specified by the caller.
michael@0 42 *
michael@0 43 * In the case of errors, we try to close the stream and File.
michael@0 44 */
michael@0 45 public class TelemetryRecorder {
michael@0 46 private final String LOG_TAG = "TelemetryRecorder";
michael@0 47
michael@0 48 private final File parentDir;
michael@0 49 private final String filename;
michael@0 50
michael@0 51 private File tmpFile;
michael@0 52 private File destFile;
michael@0 53 private File cacheDir;
michael@0 54
michael@0 55 private OutputStream outputStream;
michael@0 56 private MessageDigest checksum;
michael@0 57 private String base64Checksum;
michael@0 58
michael@0 59 /**
michael@0 60 * Charset to use for writing pings; default is us-ascii.
michael@0 61 *
michael@0 62 * When telemetry calculates the checksum for the ping file, it lossily
michael@0 63 * converts utf-16 to ascii. Therefore we have to treat characters in the
michael@0 64 * traces as ascii rather than say utf-8. Otherwise we will get a "wrong"
michael@0 65 * checksum.
michael@0 66 */
michael@0 67 private String charset = "us-ascii";
michael@0 68
michael@0 69 /**
michael@0 70 * Override blockSize in constructor if desired.
michael@0 71 * Default block size is that of BufferedOutputStream.
michael@0 72 */
michael@0 73 private int blockSize = 0;
michael@0 74
michael@0 75 /**
michael@0 76 * Constructs a TelemetryRecorder for writing a ping file. A temporary file will be written first,
michael@0 77 * and then moved to the destination file location specified by the caller.
michael@0 78 *
michael@0 79 * The directory for writing the temporary file is highly suggested to be the Android internal cache directory,
michael@0 80 * fetched by <code>context.getCacheDir()</code>
michael@0 81 *
michael@0 82 * If the destination file already exists, it will be deleted and overwritten.
michael@0 83 *
michael@0 84 * Default charset: "us-ascii"
michael@0 85 * Default block size: uses constructor default of 8192 bytes (see javadocs for
michael@0 86 * <code>BufferedOutputStream</code>
michael@0 87 * @param parentPath
michael@0 88 * path of parent directory of ping file to be written
michael@0 89 * @param cacheDir
michael@0 90 * path of cache directory for writing temporary files.
michael@0 91 * @param filename
michael@0 92 * name of ping file to be written
michael@0 93 */
michael@0 94 public TelemetryRecorder(File parentDir, File cacheDir, String filename) {
michael@0 95 if (!parentDir.isDirectory()) {
michael@0 96 throw new IllegalArgumentException("Expecting directory, got non-directory File instead.");
michael@0 97 }
michael@0 98 this.parentDir = parentDir;
michael@0 99 this.filename = filename;
michael@0 100 this.cacheDir = cacheDir;
michael@0 101 }
michael@0 102
michael@0 103 public TelemetryRecorder(File parentDir, File cacheDir, String filename, String charset) {
michael@0 104 this(parentDir, cacheDir, filename);
michael@0 105 this.charset = charset;
michael@0 106 }
michael@0 107
michael@0 108 public TelemetryRecorder(File parentDir, File cacheDir, String filename, String charset, int blockSize) {
michael@0 109 this(parentDir, cacheDir, filename, charset);
michael@0 110 this.blockSize = blockSize;
michael@0 111 }
michael@0 112
michael@0 113 /**
michael@0 114 * Start the temporary ping file and write the "slug" header and payload key, of the
michael@0 115 * format:
michael@0 116 *
michael@0 117 * { "slug": "< filename >", "payload":
michael@0 118 *
michael@0 119 * @throws Exception
michael@0 120 * Checked exceptions <code>NoSuchAlgorithmException</code>,
michael@0 121 * <code>UnsupportedEncodingException</code>, or
michael@0 122 * <code>IOException</code> and unchecked exception that
michael@0 123 * are rethrown to caller
michael@0 124 */
michael@0 125 public void startPingFile() throws Exception {
michael@0 126
michael@0 127 // Open stream to temporary file for writing.
michael@0 128 try {
michael@0 129 tmpFile = File.createTempFile(filename, "tmp", cacheDir);
michael@0 130 } catch (IOException e) {
michael@0 131 // Try to create the temporary file in the ping directory.
michael@0 132 tmpFile = new File(parentDir, filename + ".tmp");
michael@0 133 try {
michael@0 134 tmpFile.createNewFile();
michael@0 135 } catch (IOException e1) {
michael@0 136 cleanUpAndRethrow("Failed to create tmp file in temp directory or ping directory.", e1);
michael@0 137 }
michael@0 138 }
michael@0 139
michael@0 140 try {
michael@0 141 if (blockSize > 0) {
michael@0 142 outputStream = new BufferedOutputStream(new FileOutputStream(tmpFile), blockSize);
michael@0 143 } else {
michael@0 144 outputStream = new BufferedOutputStream(new FileOutputStream(tmpFile));
michael@0 145 }
michael@0 146
michael@0 147 // Create checksum for ping.
michael@0 148 checksum = MessageDigest.getInstance("SHA-256");
michael@0 149
michael@0 150 // Write ping header.
michael@0 151 byte[] header = makePingHeader(filename);
michael@0 152 outputStream.write(header);
michael@0 153 Logger.debug(LOG_TAG, "Wrote " + header.length + " header bytes.");
michael@0 154
michael@0 155 } catch (NoSuchAlgorithmException e) {
michael@0 156 cleanUpAndRethrow("Error creating checksum digest", e);
michael@0 157 } catch (UnsupportedEncodingException e) {
michael@0 158 cleanUpAndRethrow("Error writing header", e);
michael@0 159 } catch (IOException e) {
michael@0 160 cleanUpAndRethrow("Error writing to stream", e);
michael@0 161 }
michael@0 162 }
michael@0 163
michael@0 164 private byte[] makePingHeader(String slug)
michael@0 165 throws UnsupportedEncodingException {
michael@0 166 return ("{\"slug\":" + JSONObject.quote(slug) + "," + "\"payload\":\"")
michael@0 167 .getBytes(charset);
michael@0 168 }
michael@0 169
michael@0 170 /**
michael@0 171 * Append payloadContent to ping file and update the checksum.
michael@0 172 *
michael@0 173 * @param payloadContent
michael@0 174 * String content to be written
michael@0 175 * @return number of bytes written, or -1 if writing failed
michael@0 176 * @throws Exception
michael@0 177 * Checked exceptions <code>UnsupportedEncodingException</code> or
michael@0 178 * <code>IOException</code> and unchecked exception that
michael@0 179 * are rethrown to caller
michael@0 180 */
michael@0 181 public int appendPayload(String payloadContent) throws Exception {
michael@0 182 if (payloadContent == null) {
michael@0 183 cleanUpAndRethrow("Payload is null", new Exception());
michael@0 184 return -1;
michael@0 185 }
michael@0 186
michael@0 187 try {
michael@0 188 byte[] payloadBytes = payloadContent.getBytes(charset);
michael@0 189 // If we run into an error, we'll throw and abort, so checksum won't be stale.
michael@0 190 checksum.update(payloadBytes);
michael@0 191
michael@0 192 byte[] quotedPayloadBytes = JSONObject.quote(payloadContent).getBytes(charset);
michael@0 193
michael@0 194 // First and last bytes are quotes inserted by JSONObject.quote; discard
michael@0 195 // them.
michael@0 196 int numBytes = quotedPayloadBytes.length - 2;
michael@0 197 outputStream.write(quotedPayloadBytes, 1, numBytes);
michael@0 198 return numBytes;
michael@0 199
michael@0 200 } catch (UnsupportedEncodingException e) {
michael@0 201 cleanUpAndRethrow("Error encoding payload", e);
michael@0 202 return -1;
michael@0 203 } catch (IOException e) {
michael@0 204 cleanUpAndRethrow("Error writing to stream", e);
michael@0 205 return -1;
michael@0 206 }
michael@0 207 }
michael@0 208
michael@0 209 /**
michael@0 210 * Add the checksum of the payload to the ping file and close the stream.
michael@0 211 *
michael@0 212 * @throws Exception
michael@0 213 * Checked exceptions <code>UnsupportedEncodingException</code> or
michael@0 214 * <code>IOException</code> and unchecked exception that
michael@0 215 * are rethrown to caller
michael@0 216 */
michael@0 217 public void finishPingFile() throws Exception {
michael@0 218 try {
michael@0 219 byte[] footer = makePingFooter(checksum);
michael@0 220 outputStream.write(footer);
michael@0 221 // We're done writing, so force the stream to flush the buffer.
michael@0 222 outputStream.flush();
michael@0 223 Logger.debug(LOG_TAG, "Wrote " + footer.length + " footer bytes.");
michael@0 224 } catch (UnsupportedEncodingException e) {
michael@0 225 cleanUpAndRethrow("Checksum encoding exception", e);
michael@0 226 } catch (IOException e) {
michael@0 227 cleanUpAndRethrow("Error writing footer to stream", e);
michael@0 228 } finally {
michael@0 229 try {
michael@0 230 outputStream.close();
michael@0 231 } catch (IOException e) {
michael@0 232 // Failed to close, nothing we can do except discard the reference to the stream.
michael@0 233 outputStream = null;
michael@0 234 }
michael@0 235 }
michael@0 236
michael@0 237 // Move temp file to destination specified by caller.
michael@0 238 try {
michael@0 239 File destFile = new File(parentDir, filename);
michael@0 240 // Delete file if it exists - docs state that rename may fail if the File already exists.
michael@0 241 if (destFile.exists()) {
michael@0 242 destFile.delete();
michael@0 243 }
michael@0 244 boolean result = tmpFile.renameTo(destFile);
michael@0 245 if (!result) {
michael@0 246 throw new IOException("Could not move tmp file to destination.");
michael@0 247 }
michael@0 248 } finally {
michael@0 249 cleanUp();
michael@0 250 }
michael@0 251 }
michael@0 252
michael@0 253 private byte[] makePingFooter(MessageDigest checksum)
michael@0 254 throws UnsupportedEncodingException {
michael@0 255 base64Checksum = Base64.encodeToString(checksum.digest(), Base64.NO_WRAP);
michael@0 256 return ("\",\"checksum\":" + JSONObject.quote(base64Checksum) + "}")
michael@0 257 .getBytes(charset);
michael@0 258 }
michael@0 259
michael@0 260 /**
michael@0 261 * Get final digested Base64 checksum.
michael@0 262 *
michael@0 263 * @return String checksum if it has been calculated, null otherwise.
michael@0 264 */
michael@0 265 protected String getFinalChecksum() {
michael@0 266 return base64Checksum;
michael@0 267 }
michael@0 268
michael@0 269 public String getCharset() {
michael@0 270 return this.charset;
michael@0 271 }
michael@0 272
michael@0 273 /**
michael@0 274 * Clean up checksum and delete the temporary file.
michael@0 275 */
michael@0 276 private void cleanUp() {
michael@0 277 // Discard checksum.
michael@0 278 checksum.reset();
michael@0 279
michael@0 280 // Clean up files.
michael@0 281 if (tmpFile != null && tmpFile.exists()) {
michael@0 282 tmpFile.delete();
michael@0 283 }
michael@0 284 tmpFile = null;
michael@0 285 }
michael@0 286
michael@0 287 /**
michael@0 288 * Log message and error and clean up, then rethrow exception to caller.
michael@0 289 *
michael@0 290 * @param message
michael@0 291 * Error message
michael@0 292 * @param e
michael@0 293 * Exception
michael@0 294 *
michael@0 295 * @throws Exception
michael@0 296 * Exception to be rethrown to caller
michael@0 297 */
michael@0 298 private void cleanUpAndRethrow(String message, Exception e) throws Exception {
michael@0 299 Logger.error(LOG_TAG, message, e);
michael@0 300 cleanUp();
michael@0 301
michael@0 302 if (outputStream != null) {
michael@0 303 try {
michael@0 304 outputStream.close();
michael@0 305 } catch (IOException exception) {
michael@0 306 // Failed to close stream; nothing we can do, and we're aborting anyways.
michael@0 307 }
michael@0 308 }
michael@0 309
michael@0 310 if (destFile != null && destFile.exists()) {
michael@0 311 destFile.delete();
michael@0 312 }
michael@0 313 // Rethrow the exception.
michael@0 314 throw e;
michael@0 315 }
michael@0 316 }

mercurial