har-exporter.js (8063B)
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 "use strict"; 6 7 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); 8 const clipboardHelper = require("resource://devtools/shared/platform/clipboard.js"); 9 const { 10 HarUtils, 11 } = require("resource://devtools/client/netmonitor/src/har/har-utils.js"); 12 const { 13 HarBuilder, 14 } = require("resource://devtools/client/netmonitor/src/har/har-builder.js"); 15 16 const lazy = {}; 17 18 ChromeUtils.defineESModuleGetters(lazy, { 19 FileUtils: "resource://gre/modules/FileUtils.sys.mjs", 20 }); 21 22 var uid = 1; 23 24 // Helper tracer. Should be generic sharable by other modules (bug 1171927) 25 const trace = { 26 log() {}, 27 }; 28 29 /** 30 * This object represents the main public API designed to access 31 * Network export logic. Clients, such as the Network panel itself, 32 * should use this API to export collected HTTP data from the panel. 33 */ 34 const HarExporter = { 35 // Public API 36 37 /** 38 * Save collected HTTP data from the Network panel into HAR file. 39 * 40 * @param Object options 41 * Configuration object 42 * 43 * The following options are supported: 44 * 45 * - includeResponseBodies {Boolean}: If set to true, HTTP response bodies 46 * are also included in the HAR file (can produce significantly bigger 47 * amount of data). 48 * 49 * - items {Array}: List of Network requests to be exported. 50 * 51 * - jsonp {Boolean}: If set to true the export format is HARP (support 52 * for JSONP syntax). 53 * 54 * - jsonpCallback {String}: Default name of JSONP callback (used for 55 * HARP format). 56 * 57 * - compress {Boolean}: If set to true the final HAR file is zipped. 58 * This represents great disk-space optimization. 59 * 60 * - defaultFileName {String}: Default name of the target HAR file. 61 * The default file name supports the format specifier %date to output the 62 * current date/time. 63 * 64 * - defaultLogDir {String}: Default log directory for automated logs. 65 * 66 * - id {String}: ID of the page (used in the HAR file). 67 * 68 * - title {String}: Title of the page (used in the HAR file). 69 * 70 * - forceExport {Boolean}: The result HAR file is created even if 71 * there are no HTTP entries. 72 * 73 * - isSingleRequest {Boolean}: Set to true if only a single request. 74 */ 75 async save(options) { 76 // Set default options related to save operation. 77 const defaultFileName = Services.prefs.getCharPref( 78 "devtools.netmonitor.har.defaultFileName" 79 ); 80 const compress = Services.prefs.getBoolPref( 81 "devtools.netmonitor.har.compress" 82 ); 83 84 trace.log("HarExporter.save; " + defaultFileName, options); 85 86 let data = await this.fetchHarData(options); 87 88 const host = new URL(options.connector.currentTarget.url); 89 90 if (typeof options.isSingleRequest != "boolean") { 91 options.isSingleRequest = false; 92 } 93 94 const fileName = HarUtils.getHarFileName( 95 defaultFileName, 96 options.jsonp, 97 compress, 98 host.hostname, 99 options.isSingleRequest && options.items.length 100 ? new URL(options.items[0].url).pathname 101 : "" 102 ); 103 104 data = new TextEncoder().encode(data); 105 106 if (compress) { 107 const file = await DevToolsUtils.showSaveFileDialog(window, fileName, [ 108 "*.zip", 109 ]); 110 this._zip(file, fileName.replace(/\.zip$/, ""), data.buffer); 111 } else { 112 DevToolsUtils.saveAs(window, data, fileName); 113 } 114 }, 115 116 /** 117 * Helper to save the har file into a .zip file 118 * 119 * @param {nsIFIle} file The final zip file 120 * @param {string} fileName Name of the har file within the zip file 121 * @param {ArrayBuffer} buffer Content of the har file 122 */ 123 _zip(file, fileName, buffer) { 124 const ZipWriter = Components.Constructor( 125 "@mozilla.org/zipwriter;1", 126 "nsIZipWriter" 127 ); 128 const zipW = new ZipWriter(); 129 130 file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, lazy.FileUtils.PERMS_FILE); 131 132 // Open the file in write mode only and reset the size of any existing file 133 const MODE_WRONLY = 0x02; 134 const MODE_TRUNCATE = 0x20; 135 zipW.open(file, MODE_WRONLY | MODE_TRUNCATE); 136 137 const stream = Cc[ 138 "@mozilla.org/io/arraybuffer-input-stream;1" 139 ].createInstance(Ci.nsIArrayBufferInputStream); 140 stream.setData(buffer, 0, buffer.byteLength); 141 142 // Needs to be in microseconds for some reason. 143 const time = Date.now() * 1000; 144 zipW.addEntryStream(fileName, time, 0, stream, false); 145 zipW.close(); 146 }, 147 148 /** 149 * Copy HAR string into the clipboard. 150 * 151 * @param Object options 152 * Configuration object, see save() for detailed description. 153 */ 154 copy(options) { 155 return this.fetchHarData(options).then(jsonString => { 156 clipboardHelper.copyString(jsonString); 157 return jsonString; 158 }); 159 }, 160 161 /** 162 * Get HAR data as JSON object. 163 * 164 * @param Object options 165 * Configuration object, see save() for detailed description. 166 */ 167 getHar(options) { 168 return this.fetchHarData(options).then(data => { 169 return data ? JSON.parse(data) : null; 170 }); 171 }, 172 173 // Helpers 174 175 fetchHarData(options) { 176 // Generate page ID 177 options.id = options.id || uid++; 178 179 // Set default generic HAR export options. 180 if (typeof options.jsonp != "boolean") { 181 options.jsonp = Services.prefs.getBoolPref( 182 "devtools.netmonitor.har.jsonp" 183 ); 184 } 185 if (typeof options.includeResponseBodies != "boolean") { 186 options.includeResponseBodies = Services.prefs.getBoolPref( 187 "devtools.netmonitor.har.includeResponseBodies" 188 ); 189 } 190 if (typeof options.jsonpCallback != "boolean") { 191 options.jsonpCallback = Services.prefs.getCharPref( 192 "devtools.netmonitor.har.jsonpCallback" 193 ); 194 } 195 if (typeof options.forceExport != "boolean") { 196 options.forceExport = Services.prefs.getBoolPref( 197 "devtools.netmonitor.har.forceExport" 198 ); 199 } 200 if (typeof options.supportsMultiplePages != "boolean") { 201 options.supportsMultiplePages = Services.prefs.getBoolPref( 202 "devtools.netmonitor.har.multiple-pages" 203 ); 204 } 205 206 // Build HAR object. 207 return this.buildHarData(options) 208 .then(har => { 209 // Do not export an empty HAR file, unless the user 210 // explicitly says so (using the forceExport option). 211 if (!har.log.entries.length && !options.forceExport) { 212 return Promise.resolve(); 213 } 214 215 let jsonString = this.stringify(har); 216 if (!jsonString) { 217 return Promise.resolve(); 218 } 219 220 // If JSONP is wanted, wrap the string in a function call 221 if (options.jsonp) { 222 // This callback name is also used in HAR Viewer by default. 223 // http://www.softwareishard.com/har/viewer/ 224 const callbackName = options.jsonpCallback || "onInputData"; 225 jsonString = callbackName + "(" + jsonString + ");"; 226 } 227 228 return jsonString; 229 }) 230 .catch(function onError(err) { 231 console.error(err); 232 }); 233 }, 234 235 /** 236 * Build HAR data object. This object contains all HTTP data 237 * collected by the Network panel. The process is asynchronous 238 * since it can involve additional RDP communication (e.g. resolving 239 * long strings). 240 */ 241 async buildHarData(options) { 242 // Disconnect from redux actions/store. 243 options.connector.enableActions(false); 244 245 // Build HAR object from collected data. 246 const builder = new HarBuilder(options); 247 const result = await builder.build(); 248 249 // Connect to redux actions again. 250 options.connector.enableActions(true); 251 252 return result; 253 }, 254 255 /** 256 * Build JSON string from the HAR data object. 257 */ 258 stringify(har) { 259 if (!har) { 260 return null; 261 } 262 263 try { 264 return JSON.stringify(har, null, " "); 265 } catch (err) { 266 console.error(err); 267 return undefined; 268 } 269 }, 270 }; 271 272 // Exports from this module 273 exports.HarExporter = HarExporter;