|  | 
|  | 1 | +'use strict'; | 
|  | 2 | + | 
|  | 3 | +const { | 
|  | 4 | +  ArrayIsArray, | 
|  | 5 | +  DateNow, | 
|  | 6 | +  ObjectEntries, | 
|  | 7 | +  String, | 
|  | 8 | +  Symbol, | 
|  | 9 | +} = primordials; | 
|  | 10 | + | 
|  | 11 | +const { | 
|  | 12 | +  kInspectorRequestId, | 
|  | 13 | +  kResourceType, | 
|  | 14 | +  getMonotonicTime, | 
|  | 15 | +  getNextRequestId, | 
|  | 16 | +  sniffMimeType, | 
|  | 17 | +} = require('internal/inspector/network'); | 
|  | 18 | +const dc = require('diagnostics_channel'); | 
|  | 19 | +const { Network } = require('inspector'); | 
|  | 20 | +const { | 
|  | 21 | +  HTTP2_HEADER_AUTHORITY, | 
|  | 22 | +  HTTP2_HEADER_CONTENT_TYPE, | 
|  | 23 | +  HTTP2_HEADER_COOKIE, | 
|  | 24 | +  HTTP2_HEADER_METHOD, | 
|  | 25 | +  HTTP2_HEADER_PATH, | 
|  | 26 | +  HTTP2_HEADER_SCHEME, | 
|  | 27 | +  HTTP2_HEADER_SET_COOKIE, | 
|  | 28 | +  HTTP2_HEADER_STATUS, | 
|  | 29 | +  NGHTTP2_NO_ERROR, | 
|  | 30 | +} = internalBinding('http2').constants; | 
|  | 31 | + | 
|  | 32 | +const kRequestUrl = Symbol('kRequestUrl'); | 
|  | 33 | + | 
|  | 34 | +// Convert a Headers object (Map<string, number | string | string[]>) to a plain object (Map<string, string>) | 
|  | 35 | +function convertHeaderObject(headers = {}) { | 
|  | 36 | +  let scheme; | 
|  | 37 | +  let authority; | 
|  | 38 | +  let path; | 
|  | 39 | +  let method; | 
|  | 40 | +  let statusCode; | 
|  | 41 | +  let charset; | 
|  | 42 | +  let mimeType; | 
|  | 43 | +  const dict = {}; | 
|  | 44 | + | 
|  | 45 | +  for (const { 0: key, 1: value } of ObjectEntries(headers)) { | 
|  | 46 | +    const lowerCasedKey = key.toLowerCase(); | 
|  | 47 | + | 
|  | 48 | +    if (lowerCasedKey === HTTP2_HEADER_SCHEME) { | 
|  | 49 | +      scheme = value; | 
|  | 50 | +    } else if (lowerCasedKey === HTTP2_HEADER_AUTHORITY) { | 
|  | 51 | +      authority = value; | 
|  | 52 | +    } else if (lowerCasedKey === HTTP2_HEADER_PATH) { | 
|  | 53 | +      path = value; | 
|  | 54 | +    } else if (lowerCasedKey === HTTP2_HEADER_METHOD) { | 
|  | 55 | +      method = value; | 
|  | 56 | +    } else if (lowerCasedKey === HTTP2_HEADER_STATUS) { | 
|  | 57 | +      statusCode = value; | 
|  | 58 | +    } else if (lowerCasedKey === HTTP2_HEADER_CONTENT_TYPE) { | 
|  | 59 | +      const result = sniffMimeType(value); | 
|  | 60 | +      charset = result.charset; | 
|  | 61 | +      mimeType = result.mimeType; | 
|  | 62 | +    } | 
|  | 63 | + | 
|  | 64 | +    if (typeof value === 'string') { | 
|  | 65 | +      dict[key] = value; | 
|  | 66 | +    } else if (ArrayIsArray(value)) { | 
|  | 67 | +      if (lowerCasedKey === HTTP2_HEADER_COOKIE) dict[key] = value.join('; '); | 
|  | 68 | +      // ChromeDevTools frontend treats 'set-cookie' as a special case | 
|  | 69 | +      // https://github.com/ChromeDevTools/devtools-frontend/blob/4275917f84266ef40613db3c1784a25f902ea74e/front_end/core/sdk/NetworkRequest.ts#L1368 | 
|  | 70 | +      else if (lowerCasedKey === HTTP2_HEADER_SET_COOKIE) dict[key] = value.join('\n'); | 
|  | 71 | +      else dict[key] = value.join(', '); | 
|  | 72 | +    } else { | 
|  | 73 | +      dict[key] = String(value); | 
|  | 74 | +    } | 
|  | 75 | +  } | 
|  | 76 | + | 
|  | 77 | +  const url = `${scheme}://${authority}${path}`; | 
|  | 78 | + | 
|  | 79 | +  return [dict, url, method, statusCode, charset, mimeType]; | 
|  | 80 | +} | 
|  | 81 | + | 
|  | 82 | +/** | 
|  | 83 | + * When a client stream is created, emit Network.requestWillBeSent event. | 
|  | 84 | + * https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-requestWillBeSent | 
|  | 85 | + * @param {{ stream: import('http2').ClientHttp2Stream, headers: object }} event | 
|  | 86 | + */ | 
|  | 87 | +function onClientStreamCreated({ stream, headers }) { | 
|  | 88 | +  stream[kInspectorRequestId] = getNextRequestId(); | 
|  | 89 | + | 
|  | 90 | +  const { 0: convertedHeaderObject, 1: url, 2: method, 4: charset } = convertHeaderObject(headers); | 
|  | 91 | +  stream[kRequestUrl] = url; | 
|  | 92 | + | 
|  | 93 | +  Network.requestWillBeSent({ | 
|  | 94 | +    requestId: stream[kInspectorRequestId], | 
|  | 95 | +    timestamp: getMonotonicTime(), | 
|  | 96 | +    wallTime: DateNow(), | 
|  | 97 | +    charset, | 
|  | 98 | +    request: { | 
|  | 99 | +      url, | 
|  | 100 | +      method, | 
|  | 101 | +      headers: convertedHeaderObject, | 
|  | 102 | +    }, | 
|  | 103 | +  }); | 
|  | 104 | +} | 
|  | 105 | + | 
|  | 106 | +/** | 
|  | 107 | + * When a client stream errors, emit Network.loadingFailed event. | 
|  | 108 | + * https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-loadingFailed | 
|  | 109 | + * @param {{ stream: import('http2').ClientHttp2Stream, error: any }} event | 
|  | 110 | + */ | 
|  | 111 | +function onClientStreamError({ stream, error }) { | 
|  | 112 | +  if (typeof stream[kInspectorRequestId] !== 'string') { | 
|  | 113 | +    return; | 
|  | 114 | +  } | 
|  | 115 | + | 
|  | 116 | +  Network.loadingFailed({ | 
|  | 117 | +    requestId: stream[kInspectorRequestId], | 
|  | 118 | +    timestamp: getMonotonicTime(), | 
|  | 119 | +    type: kResourceType.Other, | 
|  | 120 | +    errorText: error.message, | 
|  | 121 | +  }); | 
|  | 122 | +} | 
|  | 123 | + | 
|  | 124 | +/** | 
|  | 125 | + * When response headers are received, emit Network.responseReceived event. | 
|  | 126 | + * https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-responseReceived | 
|  | 127 | + * @param {{ stream: import('http2').ClientHttp2Stream, headers: object }} event | 
|  | 128 | + */ | 
|  | 129 | +function onClientStreamFinish({ stream, headers }) { | 
|  | 130 | +  if (typeof stream[kInspectorRequestId] !== 'string') { | 
|  | 131 | +    return; | 
|  | 132 | +  } | 
|  | 133 | + | 
|  | 134 | +  const { 0: convertedHeaderObject, 3: statusCode, 4: charset, 5: mimeType } = convertHeaderObject(headers); | 
|  | 135 | + | 
|  | 136 | +  Network.responseReceived({ | 
|  | 137 | +    requestId: stream[kInspectorRequestId], | 
|  | 138 | +    timestamp: getMonotonicTime(), | 
|  | 139 | +    type: kResourceType.Other, | 
|  | 140 | +    response: { | 
|  | 141 | +      url: stream[kRequestUrl], | 
|  | 142 | +      status: statusCode, | 
|  | 143 | +      statusText: '', | 
|  | 144 | +      headers: convertedHeaderObject, | 
|  | 145 | +      mimeType, | 
|  | 146 | +      charset, | 
|  | 147 | +    }, | 
|  | 148 | +  }); | 
|  | 149 | +} | 
|  | 150 | + | 
|  | 151 | +/** | 
|  | 152 | + * When user code completes consuming the response body, emit Network.loadingFinished event. | 
|  | 153 | + * https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-loadingFinished | 
|  | 154 | + * @param {{ stream: import('http2').ClientHttp2Stream }} event | 
|  | 155 | + */ | 
|  | 156 | +function onClientStreamClose({ stream }) { | 
|  | 157 | +  if (typeof stream[kInspectorRequestId] !== 'string') { | 
|  | 158 | +    return; | 
|  | 159 | +  } | 
|  | 160 | + | 
|  | 161 | +  if (stream.rstCode !== NGHTTP2_NO_ERROR) { | 
|  | 162 | +    // This is an error case, so only Network.loadingFailed should be emitted | 
|  | 163 | +    // which is already done by onClientStreamError(). | 
|  | 164 | +    return; | 
|  | 165 | +  } | 
|  | 166 | + | 
|  | 167 | +  Network.loadingFinished({ | 
|  | 168 | +    requestId: stream[kInspectorRequestId], | 
|  | 169 | +    timestamp: getMonotonicTime(), | 
|  | 170 | +  }); | 
|  | 171 | +} | 
|  | 172 | + | 
|  | 173 | +function enable() { | 
|  | 174 | +  dc.subscribe('http2.client.stream.created', onClientStreamCreated); | 
|  | 175 | +  dc.subscribe('http2.client.stream.error', onClientStreamError); | 
|  | 176 | +  dc.subscribe('http2.client.stream.finish', onClientStreamFinish); | 
|  | 177 | +  dc.subscribe('http2.client.stream.close', onClientStreamClose); | 
|  | 178 | +} | 
|  | 179 | + | 
|  | 180 | +function disable() { | 
|  | 181 | +  dc.unsubscribe('http2.client.stream.created', onClientStreamCreated); | 
|  | 182 | +  dc.unsubscribe('http2.client.stream.error', onClientStreamError); | 
|  | 183 | +  dc.unsubscribe('http2.client.stream.finish', onClientStreamFinish); | 
|  | 184 | +  dc.unsubscribe('http2.client.stream.close', onClientStreamClose); | 
|  | 185 | +} | 
|  | 186 | + | 
|  | 187 | +module.exports = { | 
|  | 188 | +  enable, | 
|  | 189 | +  disable, | 
|  | 190 | +}; | 
0 commit comments