current commit
260610.2 · lib: add lint rule to enforce use of `kEmptyObject`
'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],
]);