diff options
Diffstat (limited to 'devtools/shared/webconsole/server-logger.js')
-rw-r--r-- | devtools/shared/webconsole/server-logger.js | 514 |
1 files changed, 514 insertions, 0 deletions
diff --git a/devtools/shared/webconsole/server-logger.js b/devtools/shared/webconsole/server-logger.js new file mode 100644 index 000000000..58a2f216a --- /dev/null +++ b/devtools/shared/webconsole/server-logger.js @@ -0,0 +1,514 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft= javascript ts=2 et sw=2 tw=80: */ +/* 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 {Ci} = require("chrome"); +const {Class} = require("sdk/core/heritage"); +const Services = require("Services"); + +const {DebuggerServer} = require("devtools/server/main"); +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); + +loader.lazyGetter(this, "NetworkHelper", () => require("devtools/shared/webconsole/network-helper")); + +// Helper tracer. Should be generic sharable by other modules (bug 1171927) +const trace = { + log: function () { + } +}; + +// Constants +const makeInfallible = DevToolsUtils.makeInfallible; +const acceptableHeaders = ["x-chromelogger-data"]; + +/** + * The listener is responsible for detecting server side logs + * within HTTP headers and sending them to the client. + * + * The logic is based on "http-on-examine-response" event that is + * sent when a response from the server is received. Consequently HTTP + * headers are parsed to find server side logs. + * + * A listeners for "http-on-examine-response" is registered when + * the listener starts and removed when destroy is executed. + */ +var ServerLoggingListener = Class({ + /** + * Initialization of the listener. The main step during the initialization + * process is registering a listener for "http-on-examine-response" event. + * + * @param {Object} win (nsIDOMWindow): + * filter network requests by the associated window object. + * If null (i.e. in the browser context) log everything + * @param {Object} owner + * The {@WebConsoleActor} instance + */ + initialize: function (win, owner) { + trace.log("ServerLoggingListener.initialize; ", owner.actorID, + ", child process: ", DebuggerServer.isInChildProcess); + + this.owner = owner; + this.window = win; + + this.onExamineResponse = this.onExamineResponse.bind(this); + this.onExamineHeaders = this.onExamineHeaders.bind(this); + this.onParentMessage = this.onParentMessage.bind(this); + + this.attach(); + }, + + /** + * The destroy is called by the parent WebConsoleActor actor. + */ + destroy: function () { + trace.log("ServerLoggingListener.destroy; ", this.owner.actorID, + ", child process: ", DebuggerServer.isInChildProcess); + + this.detach(); + }, + + /** + * The main responsibility of this method is registering a listener for + * "http-on-examine-response" events. + */ + attach: makeInfallible(function () { + trace.log("ServerLoggingListener.attach; child process: ", + DebuggerServer.isInChildProcess); + + // Setup the child <-> parent communication if this actor module + // is running in a child process. If e10s is disabled (this actor + // running in the same process as everything else) register observer + // listener just like in good old pre e10s days. + if (DebuggerServer.isInChildProcess) { + this.attachParentProcess(); + } else { + Services.obs.addObserver(this.onExamineResponse, + "http-on-examine-response", false); + } + }), + + /** + * Remove the "http-on-examine-response" listener. + */ + detach: makeInfallible(function () { + trace.log("ServerLoggingListener.detach; ", this.owner.actorID); + + if (DebuggerServer.isInChildProcess) { + this.detachParentProcess(); + } else { + Services.obs.removeObserver(this.onExamineResponse, + "http-on-examine-response", false); + } + }), + + // Parent Child Relationship + + attachParentProcess: function () { + trace.log("ServerLoggingListener.attachParentProcess;"); + + this.owner.conn.setupInParent({ + module: "devtools/shared/webconsole/server-logger-monitor", + setupParent: "setupParentProcess" + }); + + let mm = this.owner.conn.parentMessageManager; + let { addMessageListener, sendSyncMessage } = mm; + + // It isn't possible to register HTTP-* event observer inside + // a child process (in case of e10s), so listen for messages + // coming from the {@ServerLoggerMonitor} that lives inside + // the parent process. + addMessageListener("debug:server-logger", this.onParentMessage); + + // Attach to the {@ServerLoggerMonitor} object to subscribe events. + sendSyncMessage("debug:server-logger", { + method: "attachChild" + }); + }, + + detachParentProcess: makeInfallible(function () { + trace.log("ServerLoggingListener.detachParentProcess;"); + + let mm = this.owner.conn.parentMessageManager; + let { removeMessageListener, sendSyncMessage } = mm; + + sendSyncMessage("debug:server-logger", { + method: "detachChild", + }); + + removeMessageListener("debug:server-logger", this.onParentMessage); + }), + + onParentMessage: makeInfallible(function (msg) { + if (!msg.data) { + return; + } + + let method = msg.data.method; + trace.log("ServerLogger.onParentMessage; ", method, msg.data); + + switch (method) { + case "examineHeaders": + this.onExamineHeaders(msg); + break; + default: + trace.log("Unknown method name: ", method); + } + }), + + // HTTP Observer + + onExamineHeaders: function (event) { + let headers = event.data.headers; + + trace.log("ServerLoggingListener.onExamineHeaders;", headers); + + let parsedMessages = []; + + for (let item of headers) { + let header = item.header; + let value = item.value; + + let messages = this.parse(header, value); + if (messages) { + parsedMessages.push(...messages); + } + } + + if (!parsedMessages.length) { + return; + } + + for (let message of parsedMessages) { + this.sendMessage(message); + } + }, + + onExamineResponse: makeInfallible(function (subject) { + let httpChannel = subject.QueryInterface(Ci.nsIHttpChannel); + + trace.log("ServerLoggingListener.onExamineResponse; ", httpChannel.name, + ", ", this.owner.actorID, httpChannel); + + if (!this._matchRequest(httpChannel)) { + trace.log("ServerLoggerMonitor.onExamineResponse; No matching request!"); + return; + } + + let headers = []; + + httpChannel.visitResponseHeaders((header, value) => { + header = header.toLowerCase(); + if (acceptableHeaders.indexOf(header) !== -1) { + headers.push({header: header, value: value}); + } + }); + + this.onExamineHeaders({ + data: { + headers: headers, + } + }); + }), + + /** + * Check if a given network request should be logged by this network monitor + * instance based on the current filters. + * + * @private + * @param nsIHttpChannel channel + * Request to check. + * @return boolean + * True if the network request should be logged, false otherwise. + */ + _matchRequest: function (channel) { + trace.log("_matchRequest ", this.window, ", ", this.topFrame); + + // Log everything if the window is null (it's null in the browser context) + if (!this.window) { + return true; + } + + // Ignore requests from chrome or add-on code when we are monitoring + // content. + if (!channel.loadInfo && + channel.loadInfo.loadingDocument === null && + channel.loadInfo.loadingPrincipal === + Services.scriptSecurityManager.getSystemPrincipal()) { + return false; + } + + // Since frames support, this.window may not be the top level content + // frame, so that we can't only compare with win.top. + let win = NetworkHelper.getWindowForRequest(channel); + while (win) { + if (win == this.window) { + return true; + } + if (win.parent == win) { + break; + } + win = win.parent; + } + + return false; + }, + + // Server Logs + + /** + * Search through HTTP headers to catch all server side logs. + * Learn more about the data structure: + * https://craig.is/writing/chrome-logger/techspecs + */ + parse: function (header, value) { + let data; + + try { + let result = decodeURIComponent(escape(atob(value))); + data = JSON.parse(result); + } catch (err) { + console.error("Failed to parse HTTP log data! " + err); + return null; + } + + let parsedMessage = []; + let columnMap = this.getColumnMap(data); + + trace.log("ServerLoggingListener.parse; ColumnMap", columnMap); + trace.log("ServerLoggingListener.parse; data", data); + + let lastLocation; + + for (let row of data.rows) { + let backtrace = row[columnMap.get("backtrace")]; + let rawLogs = row[columnMap.get("log")]; + let type = row[columnMap.get("type")] || "log"; + + // Old version of the protocol includes a label. + // If this is the old version do some converting. + if (data.columns.indexOf("label") != -1) { + let label = row[columnMap.get("label")]; + let showLabel = label && typeof label === "string"; + + rawLogs = [rawLogs]; + + if (showLabel) { + rawLogs.unshift(label); + } + } + + // If multiple logs come from the same line only the first log + // has info about the backtrace. So, remember the last valid + // location and use it for those that not set. + let location = this.parseBacktrace(backtrace); + if (location) { + lastLocation = location; + } else { + location = lastLocation; + } + + parsedMessage.push({ + logs: rawLogs, + location: location, + type: type + }); + } + + return parsedMessage; + }, + + parseBacktrace: function (backtrace) { + if (!backtrace) { + return null; + } + + let result = backtrace.match(/\s*(\d+)$/); + if (!result || result.length < 2) { + return backtrace; + } + + return { + url: backtrace.slice(0, -result[0].length), + line: result[1] + }; + }, + + getColumnMap: function (data) { + let columnMap = new Map(); + let columnName; + + for (let key in data.columns) { + columnName = data.columns[key]; + columnMap.set(columnName, key); + } + + return columnMap; + }, + + sendMessage: function (msg) { + trace.log("ServerLoggingListener.sendMessage; message", msg); + + let formatted = format(msg); + trace.log("ServerLoggingListener.sendMessage; formatted", formatted); + + let win = this.window; + let innerID = win ? getInnerId(win) : null; + let location = msg.location; + + let message = { + category: "server", + innerID: innerID, + level: msg.type, + filename: location ? location.url : null, + lineNumber: location ? location.line : null, + columnNumber: 0, + private: false, + timeStamp: Date.now(), + arguments: formatted ? formatted.logs : null, + styles: formatted ? formatted.styles : null, + }; + + // Make sure to set the group name. + if (msg.type == "group" && formatted && formatted.logs) { + message.groupName = formatted ? formatted.logs[0] : null; + } + + // A message for console.table() method (passed in as the first + // argument) isn't supported. But, it's passed in by some server + // side libraries that implement console.* API - let's just remove it. + let args = message.arguments; + if (msg.type == "table" && args) { + if (typeof args[0] == "string") { + args.shift(); + } + } + + trace.log("ServerLoggingListener.sendMessage; raw: ", + msg.logs.join(", "), message); + + this.owner.onServerLogCall(message); + }, +}); + +// Helpers + +/** + * Parse printf-like specifiers ("%f", "%d", ...) and + * format the logs according to them. + */ +function format(msg) { + if (!msg.logs || !msg.logs[0]) { + return null; + } + + // Initialize the styles array (used for the "%c" specifier). + msg.styles = []; + + // Remove and get the first log (in which the specifiers are). + // Note that the first string doesn't have to be specified. + // An example of a log on the server side: + // ChromePhp::log("server info: ", $_SERVER); + // ChromePhp::log($_SERVER); + let firstString = ""; + if (typeof msg.logs[0] == "string") { + firstString = msg.logs.shift(); + } + + // All the specifiers present in the first string. + let splitLogRegExp = /(.*?)(%[oOcsdif]|$)/g; + let splitLogRegExpRes; + let concatString = ""; + let pushConcatString = () => { + if (concatString) { + rebuiltLogArray.push(concatString); + } + concatString = ""; + }; + + // This array represents the string of the log, in which the specifiers + // are replaced. It alternates strings and objects (%o;%O). + let rebuiltLogArray = []; + + // Get the strings before the specifiers (or the last chunk before the end + // of the string). + while ((splitLogRegExpRes = splitLogRegExp.exec(firstString)) !== null) { + let [, log, specifier] = splitLogRegExpRes; + + // We may start with a specifier or add consecutively several ones. In such + // a case, there is no log. + // Example: "%ctest" => first iteration: log = "", specifier = "%c". + // => second iteration: log = "test", specifier = "". + if (log) { + concatString += log; + } + + // Break now if there is no specifier anymore + // (means that we have reached the end of the string). + if (!specifier) { + break; + } + + let argument = msg.logs.shift(); + switch (specifier) { + case "%i": + case "%d": + // Parse into integer. + concatString += (argument | 0); + break; + case "%f": + // Parse into float. + concatString += (+argument); + break; + case "%o": + case "%O": + // Push the concatenated string and reinitialize concatString. + pushConcatString(); + // Push the object. + rebuiltLogArray.push(argument); + break; + case "%s": + concatString += argument; + break; + case "%c": + pushConcatString(); + let fillNullArrayLength = rebuiltLogArray.length - msg.styles.length; + let fillNullArray = Array(fillNullArrayLength).fill(null); + msg.styles.push(...fillNullArray, argument); + break; + } + } + + if (concatString) { + rebuiltLogArray.push(concatString); + } + + // Append the rest of arguments that don't have corresponding + // specifiers to the message logs. + msg.logs.unshift(...rebuiltLogArray); + + // Remove special ___class_name property that isn't supported + // by the current implementation. This property represents object class + // allowing custom rendering in the console panel. + for (let log of msg.logs) { + if (typeof log == "object") { + delete log.___class_name; + } + } + + return msg; +} + +// These helper are cloned from SDK to avoid loading to +// much SDK modules just because of two functions. +function getInnerId(win) { + return win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; +} + +// Exports from this module +exports.ServerLoggingListener = ServerLoggingListener; |