Skip to content
This repository has been archived by the owner on Jul 28, 2023. It is now read-only.

Feat/network #282

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
28 changes: 17 additions & 11 deletions front_end/ndb.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
{
"modules" : [
{ "name": "ndb_sdk", "type": "autostart" },
{ "name": "ndb", "type": "autostart" },
{ "name": "layer_viewer" },
{ "name": "timeline_model" },
{ "name": "timeline" },
{ "name": "product_registry" },
{ "name": "mobile_throttling" },
{ "name": "ndb_ui" },
{ "name": "xterm" }
],
"modules": [
{ "name": "ndb_sdk", "type": "autostart" },
{ "name": "ndb", "type": "autostart" },
{ "name": "layer_viewer" },
{ "name": "timeline_model" },
{ "name": "timeline" },
{ "name": "product_registry" },
{ "name": "mobile_throttling" },
{ "name": "ndb_ui" },
{ "name": "xterm" },
{ "name": "emulation", "type": "autostart" },
{ "name": "inspector_main", "type": "autostart" },
{ "name": "mobile_throttling", "type": "autostart" },
{ "name": "cookie_table" },
{ "name": "har_importer" },
{ "name": "network" }
],
"extends": "shell",
"has_html": true
}
51 changes: 50 additions & 1 deletion front_end/ndb/InspectorFrontendHostOverrides.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/

(function(){
(async function(){
InspectorFrontendHost.getPreferences = async function(callback) {
[Ndb.backend] = await carlo.loadParams();
const prefs = {
Expand All @@ -30,4 +30,53 @@
callback({statusCode: 404});
}
};

InspectorFrontendHost.sendMessageToBackend = async rawMessage => {
const parsedMes = JSON.parse(rawMessage);
if (parsedMes.method !== 'Network.getResponseBody')
return;

const mes = await target.runtimeAgent().invoke_evaluate({
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ak239 how can I have target.runtimeAgent() here?...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that we need some help from @josepharhar. We need to find some nicer way to avoid hacking network model.

In theory we can register our own network model, that will implement most stuff on top of runtime agent. Please take a look on NodeWorker or NodeRuntime models. There are examples how we can add custom models, in this case we will add our own NetworkModel implementation and I hope that DevTools frontend will use it as is.

@josepharhar do you have any suggestion how we can do it better and will it work if node target will just have own NetworkModel?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@khanghoang could you rebase your change on top of latest master? It has separated Ndb.Connection class. Alternative idea that will work to me, add Ndb.Connection.addInterceptor({sendRawMessage: function(string):boolean}). If interceptor is added than Ndb.Connection.sendRawMessage first tries to process message using one of interceptor and iff all interceptors return false will send this message to this._channel.

For Network you can add network interceptor in Ndb.NodeProcessManager.detected, something like:

Ndb.NetworkInterceptor = class {
  constructor() {
    this._buffer = [];
    this._target = null;
  }

  sendRawMessage(message) {
    if (!JSON.parse(message).method.startsWith('Network.'))
      return false;
    if (!this._target)
      this._buffer.push(message);
    else
      // do logic
  }

  setTarget(target) {
    this._target = target;
    // do logic with buffer here
  }
}

// inside detected
const connection = await Ndb.Connection.create(channel);
const interceptor = new Ndb.NetworkInterceptor();
connection.addInterceptor(interceptor);
// createTarget can send messages - it is why we need to cache them in buffer.
const target = this._targetManager.createTarget(
        info.id, userFriendlyName(info), SDK.Target.Type.Node,
        this._targetManager.targetById(info.ppid) || this._targetManager.mainTarget(), undefined, false, connection);
interceptor.setTarget(target);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having our own NetworkManager/NetworkModel for node sounds like a great idea, and seems like a step in the right direction of eventually forking the entire network panel. If I understand correctly, this means that instead of implementing the CDP interface of Network, we will instead be implementing the public methods of SDK.NetworkManager and emitting the events it emits in order to talk to the network panel. This sounds good to me because its closer to the network panel itself and is probably simpler. I'm not sure exactly how to do this within ndb, but I'm sure you know how @ak239

Copy link
Contributor

@alexkozy alexkozy Jul 16, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@khanghoang I created proof of concept of network interceptor - feel free to use it - I hope that it covers your use case - 5aef915

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ak239 @josepharhar thanks a lot. I will check it.

expression: `process._sendMessage(${JSON.stringify(JSON.parse(rawMessage))})`,
awaitPromise: true
});

if (!mes.result) return;
try {
const [id, result] = mes.result.value;
if (result) {
InspectorFrontendHost.events.dispatchEventToListeners(
InspectorFrontendHostAPI.Events.DispatchMessage,
{
id,
result
}
);
}
} catch (err) {
console.log(err);
}
};

while (true) {
const message = await target.runtimeAgent().invoke_evaluate({
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and here as well?

expression: 'process._getNetworkMessages()',
awaitPromise: true
});

if (!message.result) return;
const arrMessages = JSON.parse(message.result.value);

for (const mes of arrMessages) {
const { type, payload } = mes;

if (type) {
SDK._mainConnection._onMessage(JSON.stringify({
method: type,
params: payload
}));
}
}
}

})();
6 changes: 6 additions & 0 deletions front_end/ndb/NdbMain.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,12 @@ Ndb.NodeProcessManager = class extends Common.Object {
info.id, userFriendlyName(info), SDK.Target.Type.Node,
this._targetManager.targetById(info.ppid) || this._targetManager.mainTarget(), undefined, false, connection);
target[NdbSdk.connectionSymbol] = connection;

target.runtimeAgent().invoke_evaluate({
expression: await Ndb.backend.httpMonkeyPatchingSource(),
includeCommandLineAPI: true
});

await this.addFileSystem(info.cwd, info.scriptName);
if (info.scriptName) {
const scriptURL = Common.ParsedURL.platformPathToURL(info.scriptName);
Expand Down
6 changes: 6 additions & 0 deletions lib/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ class Backend {
opn(url);
}

async httpMonkeyPatchingSource() {
const pathToHttpPatch = path.resolve(__dirname, '..', './lib/preload/ndb/httpMonkeyPatching.js');
const content = await fsReadFile(pathToHttpPatch, 'utf8');
return content;
}

pkg() {
// TODO(ak239spb): implement it as decorations over package.json file.
try {
Expand Down
188 changes: 188 additions & 0 deletions lib/preload/ndb/httpMonkeyPatching.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
const zlib = require('zlib');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to use another way to inject this script, and another channel for inspector node process to speak with DevTools frontend. We already have channel between node process and frontend - devtools protocol.

To inject this script please do following:

  • add httpMonkeyPatchingSource method to backend.js, this method returns source of this file.
  • Ndb.NodeProcessManager.detected method gets source of this script and inject it to the page using following snippet:
target.runtimeAgent().invoke_evaluate({
  expression: await Ndb.backend.httpMonkeyPatchingSource()
});

After these steps we can inject monkey patching script to any inspected process. Second step is how we can build a channel. We can use Runtime.evaluate with awaitPromise: true flag to build a channel. Monkey patching script gets following code:

let messages = [];
let messageAdded = null;

// this function is our instrumentation, to report anything to frontend - call it instead of process.send
function reportMessage(message) {
  messages.push(message);
  if (messageAdded) {
    setTimeout(messageAdded, 0);
    messageAdded = null;
  }
}

// this function should be called from frontend in the loop using `target.runtimeAgent().invoke_evaluate`
process._getNetworkMessages = async function() {
    if (!messages.length)
      await new Promise(resolve => messageAdded = resolve);
    return messages.splice(0);
  }
}

Frontend calls in the loop following code:

while (true) {
  const messages = await target.runtimeAgent().invoke_evaluate({
    expression: 'process._getNetworkMessages()', awaitPromise: true
  });
  // ... process these messages ...
}

Feel free to ask any questions! At the same time we can merge your pull request and I will refactor it.

const http = require('http');
const https = require('https');

const initTime = process.hrtime();

// DT requires us to use relative time in a strange format (xxx.xxx)
const getTime = () => {
const diff = process.hrtime(initTime);

return diff[0] + diff[1] / 1e9;
};

const formatRequestHeaders = req => {
if (!req.headers) return {};
return Object.keys(req.headers).reduce((acc, k) => {
if (typeof req.headers[k] === 'string') acc[k] = req.headers[k];
return acc;
}, {});
};

const formatResponseHeaders = res => {
if (!res.headers) return {};
return Object.keys(res.headers).reduce((acc, k) => {
if (typeof res.headers[k] === 'string') acc[k] = res.headers[k];
return acc;
}, {});
};

const getMineType = mimeType => {
// nasty hack for ASF
if (mimeType === 'OPENJSON')
return 'application/json;charset=UTF-8';


return mimeType;
};

let cacheRequests = {};
let id = 1;
const getId = () => id++;

let messages = [];
let messageAdded = null;

function reportMessage(message) {
messages.push(message);
if (messageAdded) {
setTimeout(messageAdded, 0);
messageAdded = null;
}
}

process._getNetworkMessages = async function() {
if (!messages.length)
await new Promise(resolve => messageAdded = resolve);
return JSON.stringify(messages.splice(0));
};

process._sendMessage = async function(rawMessage) {
return new Promise(resolve => {
const message = rawMessage;
if (!cacheRequests[message.params.requestId]) {
resolve(JSON.stringify({}));
} else {
if (message.method === 'Network.getResponseBody') {
const { base64Encoded, data } = cacheRequests[message.params.requestId];

console.log({ cacheRequests });
console.log({ data });
resolve(JSON.stringify([message.id, { base64Encoded, body: data }]));
}
}
});
};

const callbackWrapper = (callback, req) => res => {
const requestId = getId();
res.req.__requestId = requestId;

reportMessage({
payload: {
requestId: requestId,
loaderId: requestId,
documentURL: req.href,
request: {
url: req.href,
method: req.method,
headers: formatRequestHeaders(req),
mixedContentType: 'none',
initialPriority: 'VeryHigh',
referrerPolicy: 'no-referrer-when-downgrade',
postData: req.body
},
timestamp: getTime(),
wallTime: Date.now(),
initiator: {
type: 'other'
},
type: 'Document'
},
type: 'Network.requestWillBeSent'
});

const encoding = res.headers['content-encoding'];
let rawData = [];

const onEnd = function() {
rawData = Buffer.concat(rawData);
rawData = rawData.toString('base64');

cacheRequests[res.req.__requestId] = {
...res,
__rawData: rawData,
base64Encoded: true
};

const payload = {
id: res.req.__requestId,
requestId: res.req.__requestId,
loaderId: res.req.__requestId,
base64Encoded: true,
data: cacheRequests[res.req.__requestId].__rawData,
timestamp: getTime(),
type: 'XHR',
encodedDataLength: 100,
response: {
url: req.href,
status: res.statusCode,
statusText: res.statusText,
// set-cookie prop in the header has value as an array
// for example: ["__cfduid=dbfe006ef71658bf4dba321343c227f9a15449556…20:29 GMT; path=/; domain=.typicode.com; HttpOnly"]
headers: formatResponseHeaders(res),
mimeType: getMineType(
res.headers['content-encoding'] ||
res.headers['content-type']
),
requestHeaders: formatRequestHeaders(req)
}
};

// Send the response back.
reportMessage({ payload: payload, type: 'Network.responseReceived' });
reportMessage({ payload: payload, type: 'Network.loadingFinished' });
};

if (encoding === 'gzip' || encoding === 'x-gzip') {
const gunzip = zlib.createGunzip();
res.pipe(gunzip);

gunzip.on('data', function(data) {
rawData.push(data);
});
gunzip.on('end', onEnd);
} else {
res.on('data', chunk => {
rawData.push(chunk);
});
res.on('end', onEnd);
}

callback && callback(res);
};

const originHTTPRequest = http.request;
http.request = function wrapMethodRequest(req, callback) {
const request = originHTTPRequest.call(
this,
req,
callbackWrapper(callback, req)
);
return request;
};

const originHTTPSRequest = https.request;
https.request = function wrapMethodRequest(req, callback) {
const request = originHTTPSRequest.call(
this,
req,
callbackWrapper(callback, req)
);
const originWrite = request.write.bind(request);
request.write = data => {
req.body = data.toString();
originWrite(data);
};
return request;
};