api.js (5825B)
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 EventEmitter = require("resource://devtools/shared/event-emitter.js"); 8 9 const { 10 bindActionCreators, 11 } = require("resource://devtools/client/shared/vendor/redux.js"); 12 const { 13 Connector, 14 } = require("resource://devtools/client/netmonitor/src/connector/index.js"); 15 const { 16 configureStore, 17 } = require("resource://devtools/client/netmonitor/src/create-store.js"); 18 const { 19 EVENTS, 20 } = require("resource://devtools/client/netmonitor/src/constants.js"); 21 const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js"); 22 23 const { 24 getDisplayedRequestById, 25 getSortedRequests, 26 } = require("resource://devtools/client/netmonitor/src/selectors/index.js"); 27 28 /** 29 * API object for NetMonitor panel (like a facade). This object can be 30 * consumed by other panels, WebExtension API, etc. 31 * 32 * This object doesn't depend on the panel UI and can be created 33 * and used even if the Network panel UI doesn't exist. 34 */ 35 class NetMonitorAPI extends EventEmitter { 36 constructor() { 37 super(); 38 39 // Connector to the backend. 40 this.connector = new Connector(); 41 42 // List of listeners for `devtools.network.onRequestFinished` WebExt API 43 this._requestFinishedListeners = new Set(); 44 45 // Bind event handlers 46 this.onPayloadReady = this.onPayloadReady.bind(this); 47 } 48 async connect(toolbox) { 49 // Bail out if already connected. 50 if (this.toolbox) { 51 return; 52 } 53 54 this.toolbox = toolbox; 55 56 // Configure store/state object. 57 this.store = configureStore( 58 this.connector, 59 this.toolbox.commands, 60 this.toolbox.telemetry 61 ); 62 this.actions = bindActionCreators(Actions, this.store.dispatch); 63 64 // Register listener for new requests (utilized by WebExtension API). 65 this.on(EVENTS.PAYLOAD_READY, this.onPayloadReady); 66 67 // Initialize connection to the backend. Pass `this` as the owner, 68 // so this object can receive all emitted events. 69 const connection = { 70 toolbox, 71 owner: this, 72 }; 73 74 await this.connector.connect(connection, this.actions, this.store.getState); 75 } 76 77 /** 78 * Clean up (unmount from DOM, remove listeners, disconnect). 79 */ 80 destroy() { 81 this.off(EVENTS.PAYLOAD_READY, this.onPayloadReady); 82 83 this.connector.disconnect(); 84 85 if (this.harExportConnector) { 86 this.harExportConnector.disconnect(); 87 } 88 } 89 90 // HAR 91 92 /** 93 * Support for `devtools.network.getHAR` (get collected data as HAR) 94 */ 95 async getHar() { 96 const { 97 HarExporter, 98 } = require("resource://devtools/client/netmonitor/src/har/har-exporter.js"); 99 const state = this.store.getState(); 100 101 const options = { 102 connector: this.connector, 103 items: getSortedRequests(state), 104 }; 105 106 return HarExporter.getHar(options); 107 } 108 109 /** 110 * Support for `devtools.network.onRequestFinished`. A hook for 111 * every finished HTTP request used by WebExtensions API. 112 */ 113 async onPayloadReady(resource) { 114 if (!this._requestFinishedListeners.size) { 115 return; 116 } 117 118 const { 119 HarExporter, 120 } = require("resource://devtools/client/netmonitor/src/har/har-exporter.js"); 121 122 const connector = await this.getHarExportConnector(); 123 const request = getDisplayedRequestById( 124 this.store.getState(), 125 resource.actor 126 ); 127 if (!request) { 128 console.error("HAR: request not found " + resource.actor); 129 return; 130 } 131 132 const options = { 133 connector, 134 includeResponseBodies: false, 135 items: [request], 136 }; 137 138 const har = await HarExporter.getHar(options); 139 140 // There is page so remove the page reference. 141 const harEntry = har.log.entries[0]; 142 delete harEntry.pageref; 143 144 this._requestFinishedListeners.forEach(listener => 145 listener({ 146 harEntry, 147 requestId: resource.actor, 148 }) 149 ); 150 } 151 152 /** 153 * Support for `Request.getContent` WebExt API (lazy loading response body) 154 */ 155 async fetchResponseContent(requestId) { 156 return this.connector.requestData(requestId, "responseContent"); 157 } 158 159 /** 160 * Add listener for `onRequestFinished` events. 161 * 162 * @param {object} listener 163 * The listener to be called it's expected to be 164 * a function that takes ({harEntry, requestId}) 165 * as first argument. 166 */ 167 addRequestFinishedListener(listener) { 168 this._requestFinishedListeners.add(listener); 169 } 170 171 removeRequestFinishedListener(listener) { 172 this._requestFinishedListeners.delete(listener); 173 } 174 175 hasRequestFinishedListeners() { 176 return this._requestFinishedListeners.size > 0; 177 } 178 179 /** 180 * Separate connector for HAR export. 181 */ 182 async getHarExportConnector() { 183 if (this.harExportConnector) { 184 // Wait for the connector to be ready to avoid exceptions if this method is called 185 // twice during its initialization. 186 await this.harExportConnectorReady; 187 return this.harExportConnector; 188 } 189 190 const connection = { 191 toolbox: this.toolbox, 192 }; 193 194 this.harExportConnector = new Connector(); 195 this.harExportConnectorReady = this.harExportConnector.connect(connection); 196 197 await this.harExportConnectorReady; 198 return this.harExportConnector; 199 } 200 201 /** 202 * Resends a given network request 203 * 204 * @param {string} requestId 205 * Id of the network request 206 */ 207 resendRequest(requestId) { 208 // Flush queued requests. 209 this.store.dispatch(Actions.batchFlush()); 210 // Send custom request with same url, headers and body as the request 211 // with the given requestId. 212 this.store.dispatch(Actions.sendCustomRequest(requestId)); 213 } 214 } 215 216 exports.NetMonitorAPI = NetMonitorAPI;