new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/connector/chrome/bulk-loader.js
@@ -0,0 +1,128 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+let bulkLoader = undefined;
+
+let PriorityLevels = {Critical: 1, Major: 2, Normal: 3, None: 0};
+
+class Scheduler {
+ constructor() {
+ this.busy = false;
+ this.queue = [];
+ }
+
+ sync(task) {
+ this.queue.push(task);
+ if (!this.busy) {
+ return this.dequeue();
+ }
+ return null;
+ }
+
+ dequeue() {
+ let self = this;
+ this.busy = true;
+ let next = this.queue.shift();
+ if (next) {
+ next().then(
+ (resolve) => {
+ self.dequeue();
+ }, (reject) => {
+ self.dequeue();
+ });
+ } else {
+ this.busy = false;
+ }
+ }
+}
+// singleton class
+const getBulkLoader = () => {
+ const mappingPriority = (priority, options) => {
+ switch (priority) {
+ case PriorityLevels.Critical:
+ return options.Critical;
+ case PriorityLevels.Major:
+ return options.Major;
+ case PriorityLevels.Normal:
+ return options.Normal;
+ case PriorityLevels.None:
+ default:
+ break;
+ }
+ return options.None;
+ };
+
+ const getTimeoutMS = (priority) => {
+ const delay = {Critical: 3000, Major: 1000, Normal: 500, None: 100};
+ return mappingPriority(priority, delay);
+ };
+
+ const getDelayStartMS = (priority) => {
+ const delay = {Critical: 1, Major: 50, Normal: 100, None: 500};
+ return mappingPriority(priority, delay);
+ };
+
+ const LoaderPromise = (priority, callback) => {
+ return new Promise((resolve, reject) => {
+ const ms = getTimeoutMS(priority);
+ // Set up the real work
+ setTimeout(() => callback(resolve, reject), getDelayStartMS(priority));
+
+ // Set up the timeout
+ setTimeout(() => {
+ reject("Promise timed out after " + ms + " ms");
+ }, ms);
+ });
+ };
+
+ // TODO : recovery thread after all tasks finished.
+ class Thread {
+ constructor() {
+ this.scheduler = new Scheduler();
+ }
+
+ addTask(callback, priority) {
+ this.scheduler.sync(() => {
+ return LoaderPromise(
+ !priority ? PriorityLevels.None : priority,
+ (resolve, reject) => callback(resolve, reject)
+ );
+ });
+ }
+ }
+
+ class BulkLoader {
+ constructor() {
+ this.threads = new Map();
+ this.tasks = [];
+ }
+
+ add(id, callback, priority) {
+ let thread = this.threads.get(priority);
+ if (!this.threads.has(priority)) {
+ thread = new Thread();
+ this.threads.set(priority, thread);
+ }
+ this.tasks.push({id, priority, task: callback, isFinished: false});
+ return thread.addTask(callback, priority);
+ }
+
+ reset() {
+ this.threads.clear();
+ }
+ }
+
+ if (!bulkLoader) {
+ bulkLoader = new BulkLoader();
+ }
+
+ return bulkLoader;
+};
+
+module.exports = {
+ getBulkLoader,
+ PriorityLevels
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/connector/chrome/events.js
@@ -0,0 +1,235 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+const {EVENTS} = require("../../constants");
+const {Payloads} = require("./utils");
+const {getBulkLoader, PriorityLevels} = require("./bulk-loader");
+
+class CDPConnector {
+ constructor() {
+ this.payloads = new Payloads();
+ this.onNetworkUpdate = this.onNetworkUpdate.bind(this);
+ this.onResponseReceived = this.onResponseReceived.bind(this);
+ this.onDataReceived = this.onDataReceived.bind(this);
+ this.onLoadingFinished = this.onLoadingFinished.bind(this);
+ this.onLoadingFailed = this.onLoadingFailed.bind(this);
+ this.update = this.update.bind(this);
+ }
+
+ setup(connection, actions) {
+ let {Network, Page} = connection;
+ this.Network = Network;
+ this.Page = Page;
+ this.actions = actions;
+ Network.requestWillBeSent(this.onNetworkUpdate);
+ Network.responseReceived(this.onResponseReceived);
+ Network.dataReceived(this.onDataReceived);
+ Network.loadingFinished(this.onLoadingFinished);
+ Network.loadingFailed(this.onLoadingFailed);
+ Network.enable();
+ Page.enable();
+ }
+
+ disconnect() {
+ this.Network.disable();
+ this.Page.disable();
+ this.payloads.clear();
+ }
+
+ async reset() {
+ return Promise.all([
+ this.Network.disable(),
+ this.Page.disable(),
+ this.payloads.clear(),
+ this.Network.enable(),
+ this.Page.enable()
+ ]);
+ }
+
+ willNavigate(event) {
+ // not support
+ }
+
+ onNetworkUpdate(params) {
+ let {requestId} = params;
+ let payload = this.payloads.add(requestId);
+ return payload.update(params).then(
+ ([request, header, postData]) => {
+ let bulkloader = getBulkLoader();
+ bulkloader.add(
+ requestId,
+ (resolve, reject) =>
+ this.addRequest(requestId, request).then(() => {
+ this.updateRequestHeader(requestId, header);
+ this.updatePostData(requestId, postData);
+ resolve();
+ })
+ , PriorityLevels.Critical);
+ });
+ }
+
+ onResponseReceived(params) {
+ let {requestId} = params;
+ let payload = this.payloads.get(requestId);
+ return payload.update(params).then(
+ ([request, header, postData, state, timings]) => {
+ let loader = getBulkLoader();
+ loader.add(
+ requestId,
+ (resolve) => {
+ this.updateResponseHeader(requestId, header);
+ this.updateResponseState(requestId, state);
+ this.updateResponseTiming(requestId, timings);
+ this.getResponseContent(params);
+ resolve();
+ }
+ , PriorityLevels.Major);
+ });
+ }
+
+ onDataReceived(params) {
+ let {requestId} = params;
+ let payload = this.payloads.get(requestId);
+ payload.update(params);
+ }
+
+ onLoadingFinished(params) {
+ let {requestId} = params;
+ let payload = this.payloads.get(requestId);
+ if (payload) {
+ payload.log("LoadingFinished", params);
+ }
+ // TODO: verify getCookie method.
+ //
+ }
+
+ updateRequestHeader(requestId, header) {
+ if (!header) {
+ return;
+ }
+ this.update(requestId, {
+ requestHeaders: header
+ }).then(() => {
+ window.emit(EVENTS.RECEIVED_REQUEST_HEADERS, header);
+ });
+ }
+
+ updateResponseTiming(requestId, timings) {
+ if (!timings) {
+ return;
+ }
+ this.update(requestId, {
+ eventTimings: timings
+ }).then(() => {
+ window.emit(EVENTS.RECEIVED_EVENT_TIMINGS, requestId);
+ });
+ }
+
+ updateResponseState(requestId, state) {
+ this.update(requestId, state).then(() => {
+ window.emit(EVENTS.STARTED_RECEIVING_RESPONSE, requestId);
+ });
+ }
+
+ updateResponseHeader(requestId, header) {
+ if (!header) {
+ return;
+ }
+ this.update(requestId, {
+ responseHeaders: header
+ }).then(() => {
+ window.emit(EVENTS.RECEIVED_RESPONSE_HEADERS, header);
+ });
+ }
+
+ onLoadingFailed(params) {
+ let {requestId} = params;
+ let payload = this.payloads.get(requestId);
+ if (payload) {
+ payload.log("LoadingFailed", params);
+ }
+ // console.log(params.requestId);
+ }
+
+ async getResponseContent(params) {
+ let {requestId, response} = params;
+
+ return this.Network.getResponseBody({requestId}).then(
+ (content) => {
+ let payload = this.payloads.get(requestId);
+ return payload.update({requestId, response, content}).then(
+ ([request, header, postData, state, timings, responseContent]) => {
+ let loader = getBulkLoader();
+ loader.add(
+ requestId,
+ (resolve) => {
+ this.updateResponseContent(requestId, responseContent);
+ return resolve();
+ },
+ PriorityLevels.Normal
+ );
+ }
+ );
+ }
+ );
+ }
+
+ updateResponseContent(requestId, payload) {
+ if (!payload) {
+ return;
+ }
+ this.actions.updateRequest(requestId, payload, true).then(
+ () => {
+ window.emit(EVENTS.RECEIVED_RESPONSE_CONTENT, requestId);
+ }
+ );
+ }
+
+ updatePostData(requestId, postData) {
+ if (!postData) {
+ return;
+ }
+ this.update(requestId, {
+ requestPostData: postData,
+ }).then(() => {
+ window.emit(EVENTS.RECEIVED_REQUEST_POST_DATA, requestId);
+ });
+ }
+
+ async update(id, payload) {
+ return this.actions.updateRequest(id, payload, true);
+ }
+
+ async addRequest(id, data) {
+ let {
+ method,
+ url,
+ isXHR,
+ cause,
+ startedDateTime,
+ fromCache,
+ fromServiceWorker,
+ } = data;
+
+ this.actions.addRequest(
+ id,
+ {
+ startedMillis: startedDateTime,
+ method,
+ url,
+ isXHR,
+ cause,
+ fromCache,
+ fromServiceWorker,
+ },
+ true,
+ )
+ .then(() => window.emit(EVENTS.REQUEST_ADDED, id));
+ }
+}
+
+module.exports = {
+ CDPConnector
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/connector/chrome/request.js
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function mappingCallFrames(callFrames) {
+ let stacktrace = [];
+ callFrames.forEach(
+ (frame) => {
+ let {
+ functionName,
+ scriptId,
+ url,
+ lineNumber,
+ columnNumber
+ } = frame;
+ let stack = {
+ scriptId,
+ filename: url,
+ lineNumber,
+ columnNumber,
+ functionName,
+ };
+ stacktrace.push(stack);
+ }
+ );
+ return stacktrace;
+}
+
+function Cause(initiator) {
+ let {url, type, stack} = initiator;
+ let {callFrames} = stack || {};
+ if (!stack || !callFrames.length) {
+ return undefined;
+ }
+ let cause = {
+ type: type,
+ loadingDocumentUri: url,
+ stacktrace: mappingCallFrames(callFrames)
+ };
+ return cause;
+}
+
+function Header(id, headers) {
+ let header = [];
+ let headersSize = 0;
+ Object.keys(headers).map((value) => {
+ header.push(
+ {
+ name: value,
+ value: headers[value],
+ }
+ );
+ headersSize += value.length + headers[value].length;
+ });
+
+ return {
+ from: id,
+ headers: header,
+ headersSize: headersSize,
+ rawHeaders: undefined,
+ };
+}
+function PostData(id, postData, header) {
+ let {headers, headersSize} = header;
+ let payload = {},
+ requestPostData = {
+ from: id, postDataDiscarded: false, postData: {}
+ };
+ if (postData) {
+ requestPostData.postData.text = postData;
+ payload.requestPostData = Object.assign({}, requestPostData);
+ payload.requestHeadersFromUploadStream = {headers, headersSize};
+ }
+ return payload;
+}
+
+/**
+ * Not support on current version.
+ * unstable method: Network.getCookies
+ * cause: https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-getCookies
+ */
+function Cookie(id, Network) {
+ // TODO: verify
+}
+
+function Request(id, requestData) {
+ let {request, initiator, timestamp} = requestData;
+ let {url, method} = request;
+ let cause = !initiator ? undefined : Cause(initiator);
+ return {
+ method, url, cause,
+ isXHR: false, // TODO: verify
+ startedDateTime: timestamp,
+ fromCache: undefined,
+ fromServiceWorker: undefined
+ };
+}
+
+module.exports = {
+ Cause,
+ Cookie,
+ Header,
+ Request,
+ PostData
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/connector/chrome/response.js
@@ -0,0 +1,102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { formDataURI } = require("../../utils/request-utils");
+
+function ResponseInfo(id, response, content) {
+ let {
+ mimeType
+ } = response;
+ const {body, base64Encoded} = content;
+ return {
+ from: id,
+ content: {
+ mimeType: mimeType,
+ text: !body ? "" : body,
+ size: !body ? 0 : body.length,
+ encoding: base64Encoded ? "base64" : undefined
+ }
+ };
+}
+
+function ResponseContent(id, response, content) {
+ const {body, base64Encoded} = content;
+ let {mimeType, encodedDataLength} = response;
+ let responseContent = ResponseInfo(id, response, content);
+ let payload = Object.assign(
+ {
+ responseContent,
+ contentSize: !body ? 0 : body.length,
+ transferredSize: encodedDataLength, // TODO: verify
+ mimeType: mimeType
+ }, body);
+ if (mimeType.includes("image/")) {
+ payload.responseContentDataUri = formDataURI(mimeType, base64Encoded, response);
+ }
+ return payload;
+}
+
+/**
+ * Not support on current version.
+ * unstable method: Security
+ * cause: https://chromedevtools.github.io/devtools-protocol/tot/Security/
+ */
+function SecurityDetails(id, security) {
+ // TODO : verify
+
+ return {};
+}
+
+function Timings(id, timing) {
+ // TODO : implement
+ let {
+ dnsStart,
+ dnsEnd,
+ connectStart,
+ connectEnd,
+ sendStart,
+ sendEnd,
+ receiveHeadersEnd
+ } = timing;
+ let dns = parseInt(dnsEnd - dnsStart, 10);
+ let connect = parseInt(connectEnd - connectStart, 10);
+ let send = parseInt(sendEnd - sendStart, 10);
+ let total = parseInt(receiveHeadersEnd, 10);
+ return {
+ from: id,
+ timings: {
+ blocked: 0,
+ dns: dns,
+ connect: connect,
+ send: send,
+ wait: parseInt(receiveHeadersEnd - (send + connect + dns), 10),
+ receive: 0,
+ },
+ totalTime: total,
+ };
+}
+function State(response, headers) {
+ let { headersSize } = headers;
+ let {
+ status,
+ statusText,
+ remoteIPAddress,
+ remotePort
+ } = response;
+ return {
+ remoteAddress: remoteIPAddress,
+ remotePort,
+ status,
+ statusText,
+ headersSize
+ };
+}
+module.exports = {
+ State,
+ Timings,
+ ResponseContent,
+ SecurityDetails
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/connector/chrome/utils.js
@@ -0,0 +1,132 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Request, Header, PostData } = require("./request");
+const { State, ResponseContent, Timings} = require("./response");
+const { getBulkLoader } = require("./bulk-loader");
+
+class Payload {
+ constructor() {
+ this.payload = {};
+ this.update = this.update.bind(this);
+ }
+ async update(payload) {
+ let { request, response, requestId, timestamp,
+ content, dataLength, encodedDataLength } = payload;
+ let {
+ headers,
+ postData,
+ timing
+ } = (request ? request : response) || {};
+
+ const header = await this.mappingHeader(requestId, headers);
+
+ this.requestId = requestId;
+
+ this.updateTimestamp(timestamp);
+ let data = await this.mappingAll(
+ requestId,
+ {
+ payload, response, postData,
+ header, content, timing,
+ dataLength, encodedDataLength
+ }
+ );
+ return data;
+ }
+
+ log(reason, info) {
+ this.updatePayload({
+ type: reason,
+ log: info
+ });
+ }
+
+ updateTimestamp(timestamp) {
+ let {request} = this.payload;
+ this.updatePayload(
+ request ? { response: timestamp } : { request: timestamp }
+ );
+ }
+
+ updatePayload(data) {
+ this.payload = Object.assign({}, this.payload, data);
+ }
+
+ async mappingAll(requestId, data) {
+ let {payload, response, postData,
+ header, content, timing,
+ dataLength, encodedDataLength } = data;
+ let [requests, headers, post,
+ status, timings, responses]
+ = await Promise.all(
+ [
+ this.mappingRequest(requestId, payload),
+ header,
+ this.mappingRequestPostData(requestId, postData, header),
+ this.mappingResponseStatus(requestId, response, header),
+ this.mappingTiming(requestId, timing),
+ this.mappingResponseContent(requestId, response, content)
+ ]);
+ this.updatePayload({
+ requests, headers, post, status, timings, responses, dataLength, encodedDataLength
+ });
+ return [ requests, headers, post, status, timings, responses ];
+ }
+
+ async mappingTiming(requestId, timing) {
+ return !timing ? undefined : Timings(requestId, timing);
+ }
+
+ async mappingRequest(requestId, payload) {
+ let {request} = payload;
+ return !request ? undefined : Request(requestId, payload);
+ }
+
+ async mappingHeader(requestId, headers) {
+ return !headers ? undefined : Header(requestId, headers);
+ }
+
+ async mappingRequestPostData(requestId, postData, headers) {
+ return !postData ? undefined : PostData(requestId, postData, headers);
+ }
+
+ async mappingResponseStatus(requestId, response, header) {
+ return !response ? undefined : State(response, header);
+ }
+
+ async mappingResponseContent(requestId, response, content) {
+ return !response || !content ?
+ undefined : ResponseContent(requestId, response, content);
+ }
+}
+class Payloads {
+ constructor() {
+ this.payloads = new Map();
+ }
+
+ add(id) {
+ if (!this.payloads.has(id)) {
+ this.payloads.set(id, new Payload());
+ }
+ return this.payloads.get(id);
+ }
+
+ get(id) {
+ return this.payloads.has(id) ?
+ this.payloads.get(id) : undefined;
+ }
+
+ clear() {
+ this.payloads.clear();
+ let loader = getBulkLoader();
+ loader.reset();
+ }
+}
+
+module.exports = {
+ Payload, Payloads
+};