GeezerVison is showing github.com — inspector: initial support websocket inspection · nodejs/node@ebe9272
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/inspector.md +42Lines changed: 42 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -598,6 +598,48 @@ This feature is only available with the `--experimental-network-inspection` flag | 598 | 598 | Broadcasts the `Network.loadingFailed` event to connected frontends. This event indicates that | 599 | 599 | HTTP request has failed to load. | 600 | 600 |
| | 601 | +### `inspector.Network.webSocketCreated([params])` | | 602 | + | | 603 | +<!-- YAML | | 604 | +added: | | 605 | + - REPLACEME | | 606 | +--> | | 607 | + | | 608 | +* `params` {Object} | | 609 | + | | 610 | +This feature is only available with the `--experimental-network-inspection` flag enabled. | | 611 | + | | 612 | +Broadcasts the `Network.webSocketCreated` event to connected frontends. This event indicates that | | 613 | +a WebSocket connection has been initiated. | | 614 | + | | 615 | +### `inspector.Network.webSocketHandshakeResponseReceived([params])` | | 616 | + | | 617 | +<!-- YAML | | 618 | +added: | | 619 | + - REPLACEME | | 620 | +--> | | 621 | + | | 622 | +* `params` {Object} | | 623 | + | | 624 | +This feature is only available with the `--experimental-network-inspection` flag enabled. | | 625 | + | | 626 | +Broadcasts the `Network.webSocketHandshakeResponseReceived` event to connected frontends. | | 627 | +This event indicates that the WebSocket handshake response has been received. | | 628 | + | | 629 | +### `inspector.Network.webSocketClosed([params])` | | 630 | + | | 631 | +<!-- YAML | | 632 | +added: | | 633 | + - REPLACEME | | 634 | +--> | | 635 | + | | 636 | +* `params` {Object} | | 637 | + | | 638 | +This feature is only available with the `--experimental-network-inspection` flag enabled. | | 639 | + | | 640 | +Broadcasts the `Network.webSocketClosed` event to connected frontends. | | 641 | +This event indicates that a WebSocket connection has been closed. | | 642 | + | 601 | 643 | ### `inspector.NetworkResources.put` | 602 | 644 |
| 603 | 645 | <!-- YAML | |
Collapse file Copy file name to clipboardExpand all lines: lib/inspector.js +4Lines changed: 4 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -219,6 +219,10 @@ const Network = { | 219 | 219 | loadingFailed: (params) => broadcastToFrontend('Network.loadingFailed', params), | 220 | 220 | dataSent: (params) => broadcastToFrontend('Network.dataSent', params), | 221 | 221 | dataReceived: (params) => broadcastToFrontend('Network.dataReceived', params), | | 222 | + webSocketCreated: (params) => broadcastToFrontend('Network.webSocketCreated', params), | | 223 | + webSocketClosed: (params) => broadcastToFrontend('Network.webSocketClosed', params), | | 224 | + webSocketHandshakeResponseReceived: | | 225 | + (params) => broadcastToFrontend('Network.webSocketHandshakeResponseReceived', params), | 222 | 226 | }; | 223 | 227 |
| 224 | 228 | const NetworkResources = { | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/inspector/network_undici.js +37Lines changed: 37 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -206,6 +206,39 @@ function onClientResponseFinish({ request }) { | 206 | 206 | }); | 207 | 207 | } | 208 | 208 |
| | 209 | +// TODO: Move Network.webSocketCreated to the actual creation time of the WebSocket. | | 210 | +// undici:websocket:open fires when the connection is established, but this results | | 211 | +// in an inaccurate stack trace. | | 212 | +function onWebSocketOpen({ websocket }) { | | 213 | + websocket[kInspectorRequestId] = getNextRequestId(); | | 214 | + const url = websocket.url.toString(); | | 215 | + Network.webSocketCreated({ | | 216 | + requestId: websocket[kInspectorRequestId], | | 217 | + url, | | 218 | + }); | | 219 | + // TODO: Use handshake response data from undici diagnostics when available. | | 220 | + // https://github.com/nodejs/undici/pull/4396 | | 221 | + Network.webSocketHandshakeResponseReceived({ | | 222 | + requestId: websocket[kInspectorRequestId], | | 223 | + timestamp: getMonotonicTime(), | | 224 | + response: { | | 225 | + status: 101, | | 226 | + statusText: 'Switching Protocols', | | 227 | + headers: {}, | | 228 | + }, | | 229 | + }); | | 230 | +} | | 231 | + | | 232 | +function onWebSocketClose({ websocket }) { | | 233 | + if (typeof websocket[kInspectorRequestId] !== 'string') { | | 234 | + return; | | 235 | + } | | 236 | + Network.webSocketClosed({ | | 237 | + requestId: websocket[kInspectorRequestId], | | 238 | + timestamp: getMonotonicTime(), | | 239 | + }); | | 240 | +} | | 241 | + | 209 | 242 | function enable() { | 210 | 243 | dc.subscribe('undici:request:create', onClientRequestStart); | 211 | 244 | dc.subscribe('undici:request:error', onClientRequestError); | @@ -214,6 +247,8 @@ function enable() { | 214 | 247 | dc.subscribe('undici:request:bodyChunkSent', onClientRequestBodyChunkSent); | 215 | 248 | dc.subscribe('undici:request:bodySent', onClientRequestBodySent); | 216 | 249 | dc.subscribe('undici:request:bodyChunkReceived', onClientRequestBodyChunkReceived); | | 250 | + dc.subscribe('undici:websocket:open', onWebSocketOpen); | | 251 | + dc.subscribe('undici:websocket:close', onWebSocketClose); | 217 | 252 | } | 218 | 253 |
| 219 | 254 | function disable() { | @@ -224,6 +259,8 @@ function disable() { | 224 | 259 | dc.unsubscribe('undici:request:bodyChunkSent', onClientRequestBodyChunkSent); | 225 | 260 | dc.unsubscribe('undici:request:bodySent', onClientRequestBodySent); | 226 | 261 | dc.unsubscribe('undici:request:bodyChunkReceived', onClientRequestBodyChunkReceived); | | 262 | + dc.unsubscribe('undici:websocket:open', onWebSocketOpen); | | 263 | + dc.unsubscribe('undici:websocket:close', onWebSocketClose); | 227 | 264 | } | 228 | 265 |
| 229 | 266 | module.exports = { | |
Collapse file Copy file name to clipboardExpand all lines: src/inspector/network_agent.cc +87Lines changed: 87 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -208,6 +208,35 @@ std::unique_ptr<protocol::Network::Response> createResponseFromObject( | 208 | 208 | .build(); | 209 | 209 | } | 210 | 210 |
| | 211 | +std::unique_ptr<protocol::Network::WebSocketResponse> createWebSocketResponse( | | 212 | + v8::Local<v8::Context> context, Local<Object> response) { | | 213 | + HandleScope handle_scope(context->GetIsolate()); | | 214 | + int status; | | 215 | + if (!ObjectGetInt(context, response, "status").To(&status)) { | | 216 | + return {}; | | 217 | + } | | 218 | + protocol::String statusText; | | 219 | + if (!ObjectGetProtocolString(context, response, "statusText") | | 220 | + .To(&statusText)) { | | 221 | + return {}; | | 222 | + } | | 223 | + Local<Object> headers_obj; | | 224 | + if (!ObjectGetObject(context, response, "headers").ToLocal(&headers_obj)) { | | 225 | + return {}; | | 226 | + } | | 227 | + std::unique_ptr<protocol::Network::Headers> headers = | | 228 | + createHeadersFromObject(context, headers_obj); | | 229 | + if (!headers) { | | 230 | + return {}; | | 231 | + } | | 232 | + | | 233 | + return protocol::Network::WebSocketResponse::create() | | 234 | + .setStatus(status) | | 235 | + .setStatusText(statusText) | | 236 | + .setHeaders(std::move(headers)) | | 237 | + .build(); | | 238 | +} | | 239 | + | 211 | 240 | NetworkAgent::NetworkAgent( | 212 | 241 | NetworkInspector* inspector, | 213 | 242 | v8_inspector::V8Inspector* v8_inspector, | @@ -223,6 +252,64 @@ NetworkAgent::NetworkAgent( | 223 | 252 | event_notifier_map_["loadingFinished"] = &NetworkAgent::loadingFinished; | 224 | 253 | event_notifier_map_["dataSent"] = &NetworkAgent::dataSent; | 225 | 254 | event_notifier_map_["dataReceived"] = &NetworkAgent::dataReceived; | | 255 | + event_notifier_map_["webSocketCreated"] = &NetworkAgent::webSocketCreated; | | 256 | + event_notifier_map_["webSocketClosed"] = &NetworkAgent::webSocketClosed; | | 257 | + event_notifier_map_["webSocketHandshakeResponseReceived"] = | | 258 | + &NetworkAgent::webSocketHandshakeResponseReceived; | | 259 | +} | | 260 | + | | 261 | +void NetworkAgent::webSocketCreated(v8::Local<v8::Context> context, | | 262 | + v8::Local<v8::Object> params) { | | 263 | + protocol::String request_id; | | 264 | + if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) { | | 265 | + return; | | 266 | + } | | 267 | + protocol::String url; | | 268 | + if (!ObjectGetProtocolString(context, params, "url").To(&url)) { | | 269 | + return; | | 270 | + } | | 271 | + std::unique_ptr<protocol::Network::Initiator> initiator = | | 272 | + protocol::Network::Initiator::create() | | 273 | + .setType(protocol::Network::Initiator::TypeEnum::Script) | | 274 | + .setStack( | | 275 | + v8_inspector_->captureStackTrace(true)->buildInspectorObject(0)) | | 276 | + .build(); | | 277 | + frontend_->webSocketCreated(request_id, url, std::move(initiator)); | | 278 | +} | | 279 | + | | 280 | +void NetworkAgent::webSocketClosed(v8::Local<v8::Context> context, | | 281 | + v8::Local<v8::Object> params) { | | 282 | + protocol::String request_id; | | 283 | + if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) { | | 284 | + return; | | 285 | + } | | 286 | + double timestamp; | | 287 | + if (!ObjectGetDouble(context, params, "timestamp").To(×tamp)) { | | 288 | + return; | | 289 | + } | | 290 | + frontend_->webSocketClosed(request_id, timestamp); | | 291 | +} | | 292 | + | | 293 | +void NetworkAgent::webSocketHandshakeResponseReceived( | | 294 | + v8::Local<v8::Context> context, v8::Local<v8::Object> params) { | | 295 | + protocol::String request_id; | | 296 | + if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) { | | 297 | + return; | | 298 | + } | | 299 | + double timestamp; | | 300 | + if (!ObjectGetDouble(context, params, "timestamp").To(×tamp)) { | | 301 | + return; | | 302 | + } | | 303 | + Local<Object> response_obj; | | 304 | + if (!ObjectGetObject(context, params, "response").ToLocal(&response_obj)) { | | 305 | + return; | | 306 | + } | | 307 | + auto response = createWebSocketResponse(context, response_obj); | | 308 | + if (!response) { | | 309 | + return; | | 310 | + } | | 311 | + frontend_->webSocketHandshakeResponseReceived( | | 312 | + request_id, timestamp, std::move(response)); | 226 | 313 | } | 227 | 314 |
| 228 | 315 | void NetworkAgent::emitNotification(v8::Local<v8::Context> context, | |
Collapse file Copy file name to clipboardExpand all lines: src/inspector/network_agent.h +7Lines changed: 7 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -93,6 +93,13 @@ class NetworkAgent : public protocol::Network::Backend { | 93 | 93 | void dataReceived(v8::Local<v8::Context> context, | 94 | 94 | v8::Local<v8::Object> params); | 95 | 95 |
| | 96 | + void webSocketCreated(v8::Local<v8::Context> context, | | 97 | + v8::Local<v8::Object> params); | | 98 | + void webSocketClosed(v8::Local<v8::Context> context, | | 99 | + v8::Local<v8::Object> params); | | 100 | + void webSocketHandshakeResponseReceived(v8::Local<v8::Context> context, | | 101 | + v8::Local<v8::Object> params); | | 102 | + | 96 | 103 | private: | 97 | 104 | NetworkInspector* inspector_; | 98 | 105 | v8_inspector::V8Inspector* v8_inspector_; | |
Collapse file Copy file name to clipboardExpand all lines: src/inspector/node_protocol.pdl +35Lines changed: 35 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -185,6 +185,16 @@ experimental domain Network | 185 | 185 | boolean success | 186 | 186 | optional IO.StreamHandle stream | 187 | 187 |
| | 188 | + # WebSocket response data. | | 189 | + type WebSocketResponse extends object | | 190 | + properties | | 191 | + # HTTP response status code. | | 192 | + integer status | | 193 | + # HTTP response status text. | | 194 | + string statusText | | 195 | + # HTTP response headers. | | 196 | + Headers headers | | 197 | + | 188 | 198 | # Disables network tracking, prevents network events from being sent to the client. | 189 | 199 | command disable | 190 | 200 |
| @@ -285,6 +295,31 @@ experimental domain Network | 285 | 295 | integer encodedDataLength | 286 | 296 | # Data that was received. | 287 | 297 | experimental optional binary data | | 298 | + # Fired upon WebSocket creation. | | 299 | + event webSocketCreated | | 300 | + parameters | | 301 | + # Request identifier. | | 302 | + RequestId requestId | | 303 | + # WebSocket request URL. | | 304 | + string url | | 305 | + # Request initiator. | | 306 | + Initiator initiator | | 307 | + # Fired when WebSocket is closed. | | 308 | + event webSocketClosed | | 309 | + parameters | | 310 | + # Request identifier. | | 311 | + RequestId requestId | | 312 | + # Timestamp. | | 313 | + MonotonicTime timestamp | | 314 | + # Fired when WebSocket handshake response becomes available. | | 315 | + event webSocketHandshakeResponseReceived | | 316 | + parameters | | 317 | + # Request identifier. | | 318 | + RequestId requestId | | 319 | + # Timestamp. | | 320 | + MonotonicTime timestamp | | 321 | + # WebSocket response data. | | 322 | + WebSocketResponse response | 288 | 323 |
| 289 | 324 | # Support for inspecting node process state. | 290 | 325 | experimental domain NodeRuntime | |
Collapse file Copy file name to clipboard +105Lines changed: 105 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | +const common = require('./index'); | | 3 | +if (!common.hasCrypto) | | 4 | + common.skip('missing crypto'); | | 5 | +const http = require('http'); | | 6 | +const crypto = require('crypto'); | | 7 | + | | 8 | +class WebSocketServer { | | 9 | + constructor({ | | 10 | + port = 0, | | 11 | + }) { | | 12 | + this.port = port; | | 13 | + this.server = http.createServer(); | | 14 | + this.clients = new Set(); | | 15 | + | | 16 | + this.server.on('upgrade', this.handleUpgrade.bind(this)); | | 17 | + } | | 18 | + | | 19 | + start() { | | 20 | + return new Promise((resolve) => { | | 21 | + this.server.listen(this.port, () => { | | 22 | + this.port = this.server.address().port; | | 23 | + resolve(); | | 24 | + }); | | 25 | + }).catch((err) => { | | 26 | + console.error('Failed to start WebSocket server:', err); | | 27 | + }); | | 28 | + } | | 29 | + | | 30 | + handleUpgrade(req, socket, head) { | | 31 | + const key = req.headers['sec-websocket-key']; | | 32 | + const acceptKey = this.generateAcceptValue(key); | | 33 | + const responseHeaders = [ | | 34 | + 'HTTP/1.1 101 Switching Protocols', | | 35 | + 'Upgrade: websocket', | | 36 | + 'Connection: Upgrade', | | 37 | + `Sec-WebSocket-Accept: ${acceptKey}`, | | 38 | + ]; | | 39 | + | | 40 | + socket.write(responseHeaders.join('\r\n') + '\r\n\r\n'); | | 41 | + this.clients.add(socket); | | 42 | + | | 43 | + socket.on('data', (buffer) => { | | 44 | + const opcode = buffer[0] & 0x0f; | | 45 | + | | 46 | + if (opcode === 0x8) { | | 47 | + socket.end(); | | 48 | + this.clients.delete(socket); | | 49 | + return; | | 50 | + } | | 51 | + | | 52 | + socket.write(this.encodeMessage('Hello from server!')); | | 53 | + }); | | 54 | + | | 55 | + socket.on('close', () => { | | 56 | + this.clients.delete(socket); | | 57 | + }); | | 58 | + | | 59 | + socket.on('error', (err) => { | | 60 | + console.error('Socket error:', err); | | 61 | + this.clients.delete(socket); | | 62 | + }); | | 63 | + } | | 64 | + | | 65 | + generateAcceptValue(secWebSocketKey) { | | 66 | + return crypto | | 67 | + .createHash('sha1') | | 68 | + .update(secWebSocketKey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary') | | 69 | + .digest('base64'); | | 70 | + } | | 71 | + | | 72 | + decodeMessage(buffer) { | | 73 | + const secondByte = buffer[1]; | | 74 | + const length = secondByte & 127; | | 75 | + const maskStart = 2; | | 76 | + const dataStart = maskStart + 4; | | 77 | + const masks = buffer.slice(maskStart, dataStart); | | 78 | + const data = buffer.slice(dataStart, dataStart + length); | | 79 | + const result = Buffer.alloc(length); | | 80 | + | | 81 | + for (let i = 0; i < length; i++) { | | 82 | + result[i] = data[i] ^ masks[i % 4]; | | 83 | + } | | 84 | + | | 85 | + return result.toString(); | | 86 | + } | | 87 | + | | 88 | + encodeMessage(message) { | | 89 | + const msgBuffer = Buffer.from(message); | | 90 | + const length = msgBuffer.length; | | 91 | + const frame = [0x81]; | | 92 | + | | 93 | + if (length < 126) { | | 94 | + frame.push(length); | | 95 | + } else if (length < 65536) { | | 96 | + frame.push(126, (length >> 8) & 0xff, length & 0xff); | | 97 | + } else { | | 98 | + throw new Error('Message too long'); | | 99 | + } | | 100 | + | | 101 | + return Buffer.concat([Buffer.from(frame), msgBuffer]); | | 102 | + } | | 103 | +} | | 104 | + | | 105 | +module.exports = WebSocketServer; |
|