Signed-off-by: Antoine du Hamel <duhamelantoine1995@gmail.com> PR-URL: https://github.com/nodejs/node/pull/63790 Reviewed-By: Filip Skokan <panva.ip@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Jake Yuesong Li <jake.yuesong@gmail.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
262 lines
7.8 KiB
JavaScript
262 lines
7.8 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
ArrayIsArray,
|
|
DateNow,
|
|
ObjectEntries,
|
|
String,
|
|
Symbol,
|
|
} = primordials;
|
|
|
|
const {
|
|
kInspectorRequestId,
|
|
kResourceType,
|
|
getMonotonicTime,
|
|
getNextRequestId,
|
|
registerDiagnosticChannels,
|
|
sniffMimeType,
|
|
} = require('internal/inspector/network');
|
|
const { Network } = require('inspector');
|
|
const {
|
|
HTTP2_HEADER_AUTHORITY,
|
|
HTTP2_HEADER_CONTENT_TYPE,
|
|
HTTP2_HEADER_COOKIE,
|
|
HTTP2_HEADER_METHOD,
|
|
HTTP2_HEADER_PATH,
|
|
HTTP2_HEADER_SCHEME,
|
|
HTTP2_HEADER_SET_COOKIE,
|
|
HTTP2_HEADER_STATUS,
|
|
NGHTTP2_NO_ERROR,
|
|
} = internalBinding('http2').constants;
|
|
const EventEmitter = require('events');
|
|
const { Buffer } = require('buffer');
|
|
const { kEmptyObject } = require('internal/util');
|
|
|
|
const kRequestUrl = Symbol('kRequestUrl');
|
|
|
|
// Convert a Headers object (Map<string, number | string | string[]>) to a plain object (Map<string, string>)
|
|
function convertHeaderObject(headers = kEmptyObject) {
|
|
let scheme;
|
|
let authority;
|
|
let path;
|
|
let method;
|
|
let statusCode;
|
|
let charset;
|
|
let mimeType;
|
|
const dict = {};
|
|
|
|
for (const { 0: key, 1: value } of ObjectEntries(headers)) {
|
|
const lowerCasedKey = key.toLowerCase();
|
|
|
|
if (lowerCasedKey === HTTP2_HEADER_SCHEME) {
|
|
scheme = value;
|
|
} else if (lowerCasedKey === HTTP2_HEADER_AUTHORITY) {
|
|
authority = value;
|
|
} else if (lowerCasedKey === HTTP2_HEADER_PATH) {
|
|
path = value;
|
|
} else if (lowerCasedKey === HTTP2_HEADER_METHOD) {
|
|
method = value;
|
|
} else if (lowerCasedKey === HTTP2_HEADER_STATUS) {
|
|
statusCode = value;
|
|
} else if (lowerCasedKey === HTTP2_HEADER_CONTENT_TYPE) {
|
|
const result = sniffMimeType(value);
|
|
charset = result.charset;
|
|
mimeType = result.mimeType;
|
|
}
|
|
|
|
if (typeof value === 'string') {
|
|
dict[key] = value;
|
|
} else if (ArrayIsArray(value)) {
|
|
if (lowerCasedKey === HTTP2_HEADER_COOKIE) dict[key] = value.join('; ');
|
|
// ChromeDevTools frontend treats 'set-cookie' as a special case
|
|
// https://github.com/ChromeDevTools/devtools-frontend/blob/4275917f84266ef40613db3c1784a25f902ea74e/front_end/core/sdk/NetworkRequest.ts#L1368
|
|
else if (lowerCasedKey === HTTP2_HEADER_SET_COOKIE) dict[key] = value.join('\n');
|
|
else dict[key] = value.join(', ');
|
|
} else {
|
|
dict[key] = String(value);
|
|
}
|
|
}
|
|
|
|
const url = `${scheme}://${authority}${path}`;
|
|
|
|
return [dict, url, method, statusCode, charset, mimeType];
|
|
}
|
|
|
|
/**
|
|
* When a client stream is created, emit Network.requestWillBeSent event.
|
|
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-requestWillBeSent
|
|
* @param {{ stream: import('http2').ClientHttp2Stream, headers: object }} event
|
|
*/
|
|
function onClientStreamCreated({ stream, headers }) {
|
|
stream[kInspectorRequestId] = getNextRequestId();
|
|
|
|
const { 0: convertedHeaderObject, 1: url, 2: method, 4: charset } = convertHeaderObject(headers);
|
|
stream[kRequestUrl] = url;
|
|
|
|
Network.requestWillBeSent({
|
|
requestId: stream[kInspectorRequestId],
|
|
timestamp: getMonotonicTime(),
|
|
wallTime: DateNow(),
|
|
charset,
|
|
request: {
|
|
url,
|
|
method,
|
|
headers: convertedHeaderObject,
|
|
hasPostData: !stream.writableEnded,
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* When a client stream errors, emit Network.loadingFailed event.
|
|
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-loadingFailed
|
|
* @param {{ stream: import('http2').ClientHttp2Stream, error: any }} event
|
|
*/
|
|
function onClientStreamError({ stream, error }) {
|
|
if (typeof stream[kInspectorRequestId] !== 'string') {
|
|
return;
|
|
}
|
|
|
|
Network.loadingFailed({
|
|
requestId: stream[kInspectorRequestId],
|
|
timestamp: getMonotonicTime(),
|
|
type: kResourceType.Other,
|
|
errorText: error.message,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* When a chunk of the request body is being sent, cache it until `getRequestPostData` request.
|
|
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#method-getRequestPostData
|
|
* @param {{
|
|
* stream: import('http2').ClientHttp2Stream,
|
|
* writev: boolean,
|
|
* data: Buffer | string | Array<Buffer | {chunk: Buffer|string, encoding: string}>,
|
|
* encoding: string,
|
|
* }} event
|
|
*/
|
|
function onClientStreamBodyChunkSent({ stream, writev, data, encoding }) {
|
|
if (typeof stream[kInspectorRequestId] !== 'string') {
|
|
return;
|
|
}
|
|
|
|
let chunk;
|
|
|
|
if (writev) {
|
|
if (data.allBuffers) {
|
|
chunk = Buffer.concat(data);
|
|
} else {
|
|
const buffers = [];
|
|
for (let i = 0; i < data.length; ++i) {
|
|
if (typeof data[i].chunk === 'string') {
|
|
buffers.push(Buffer.from(data[i].chunk, data[i].encoding));
|
|
} else {
|
|
buffers.push(data[i].chunk);
|
|
}
|
|
}
|
|
chunk = Buffer.concat(buffers);
|
|
}
|
|
} else if (typeof data === 'string') {
|
|
chunk = Buffer.from(data, encoding);
|
|
} else {
|
|
chunk = data;
|
|
}
|
|
|
|
Network.dataSent({
|
|
requestId: stream[kInspectorRequestId],
|
|
timestamp: getMonotonicTime(),
|
|
dataLength: chunk.byteLength,
|
|
data: chunk,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Mark a request body as fully sent.
|
|
* @param {{ stream: import('http2').ClientHttp2Stream }} event
|
|
*/
|
|
function onClientStreamBodySent({ stream }) {
|
|
if (typeof stream[kInspectorRequestId] !== 'string') {
|
|
return;
|
|
}
|
|
|
|
Network.dataSent({
|
|
requestId: stream[kInspectorRequestId],
|
|
finished: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* When response headers are received, emit Network.responseReceived event.
|
|
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-responseReceived
|
|
* @param {{ stream: import('http2').ClientHttp2Stream, headers: object }} event
|
|
*/
|
|
function onClientStreamFinish({ stream, headers }) {
|
|
if (typeof stream[kInspectorRequestId] !== 'string') {
|
|
return;
|
|
}
|
|
|
|
const { 0: convertedHeaderObject, 3: statusCode, 4: charset, 5: mimeType } = convertHeaderObject(headers);
|
|
|
|
Network.responseReceived({
|
|
requestId: stream[kInspectorRequestId],
|
|
timestamp: getMonotonicTime(),
|
|
type: kResourceType.Other,
|
|
response: {
|
|
url: stream[kRequestUrl],
|
|
status: statusCode,
|
|
statusText: '',
|
|
headers: convertedHeaderObject,
|
|
mimeType,
|
|
charset,
|
|
},
|
|
});
|
|
|
|
// Unlike stream.on('data', ...), this does not put the stream into flowing mode.
|
|
EventEmitter.prototype.on.call(stream, 'data', (chunk) => {
|
|
/**
|
|
* When a chunk of the response body has been received, cache it until `getResponseBody` request
|
|
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#method-getResponseBody or
|
|
* stream it with `streamResourceContent` request.
|
|
* https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-streamResourceContent
|
|
*/
|
|
|
|
Network.dataReceived({
|
|
requestId: stream[kInspectorRequestId],
|
|
timestamp: getMonotonicTime(),
|
|
dataLength: chunk.byteLength,
|
|
encodedDataLength: chunk.byteLength,
|
|
data: chunk,
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* When user code completes consuming the response body, emit Network.loadingFinished event.
|
|
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-loadingFinished
|
|
* @param {{ stream: import('http2').ClientHttp2Stream }} event
|
|
*/
|
|
function onClientStreamClose({ stream }) {
|
|
if (typeof stream[kInspectorRequestId] !== 'string') {
|
|
return;
|
|
}
|
|
|
|
if (stream.rstCode !== NGHTTP2_NO_ERROR) {
|
|
// This is an error case, so only Network.loadingFailed should be emitted
|
|
// which is already done by onClientStreamError().
|
|
return;
|
|
}
|
|
|
|
Network.loadingFinished({
|
|
requestId: stream[kInspectorRequestId],
|
|
timestamp: getMonotonicTime(),
|
|
});
|
|
}
|
|
|
|
module.exports = registerDiagnosticChannels([
|
|
['http2.client.stream.created', onClientStreamCreated],
|
|
['http2.client.stream.error', onClientStreamError],
|
|
['http2.client.stream.finish', onClientStreamFinish],
|
|
['http2.client.stream.close', onClientStreamClose],
|
|
['http2.client.stream.bodyChunkSent', onClientStreamBodyChunkSent],
|
|
['http2.client.stream.bodySent', onClientStreamBodySent],
|
|
]);
|