Skip to content

Commit ebe9272

Browse files
islandryutargos
authored andcommitted
inspector: initial support websocket inspection
Refs: #53946 PR-URL: #59404 Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent 15ae21b commit ebe9272

File tree

9 files changed

+409
-1
lines changed

9 files changed

+409
-1
lines changed

doc/api/inspector.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,48 @@ This feature is only available with the `--experimental-network-inspection` flag
598598
Broadcasts the `Network.loadingFailed` event to connected frontends. This event indicates that
599599
HTTP request has failed to load.
600600

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+
601643
### `inspector.NetworkResources.put`
602644

603645
<!-- YAML

lib/inspector.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ const Network = {
219219
loadingFailed: (params) => broadcastToFrontend('Network.loadingFailed', params),
220220
dataSent: (params) => broadcastToFrontend('Network.dataSent', params),
221221
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),
222226
};
223227

224228
const NetworkResources = {

lib/internal/inspector/network_undici.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,39 @@ function onClientResponseFinish({ request }) {
206206
});
207207
}
208208

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+
209242
function enable() {
210243
dc.subscribe('undici:request:create', onClientRequestStart);
211244
dc.subscribe('undici:request:error', onClientRequestError);
@@ -214,6 +247,8 @@ function enable() {
214247
dc.subscribe('undici:request:bodyChunkSent', onClientRequestBodyChunkSent);
215248
dc.subscribe('undici:request:bodySent', onClientRequestBodySent);
216249
dc.subscribe('undici:request:bodyChunkReceived', onClientRequestBodyChunkReceived);
250+
dc.subscribe('undici:websocket:open', onWebSocketOpen);
251+
dc.subscribe('undici:websocket:close', onWebSocketClose);
217252
}
218253

219254
function disable() {
@@ -224,6 +259,8 @@ function disable() {
224259
dc.unsubscribe('undici:request:bodyChunkSent', onClientRequestBodyChunkSent);
225260
dc.unsubscribe('undici:request:bodySent', onClientRequestBodySent);
226261
dc.unsubscribe('undici:request:bodyChunkReceived', onClientRequestBodyChunkReceived);
262+
dc.unsubscribe('undici:websocket:open', onWebSocketOpen);
263+
dc.unsubscribe('undici:websocket:close', onWebSocketClose);
227264
}
228265

229266
module.exports = {

src/inspector/network_agent.cc

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,35 @@ std::unique_ptr<protocol::Network::Response> createResponseFromObject(
208208
.build();
209209
}
210210

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+
211240
NetworkAgent::NetworkAgent(
212241
NetworkInspector* inspector,
213242
v8_inspector::V8Inspector* v8_inspector,
@@ -223,6 +252,64 @@ NetworkAgent::NetworkAgent(
223252
event_notifier_map_["loadingFinished"] = &NetworkAgent::loadingFinished;
224253
event_notifier_map_["dataSent"] = &NetworkAgent::dataSent;
225254
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(&timestamp)) {
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(&timestamp)) {
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));
226313
}
227314

228315
void NetworkAgent::emitNotification(v8::Local<v8::Context> context,

src/inspector/network_agent.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ class NetworkAgent : public protocol::Network::Backend {
9393
void dataReceived(v8::Local<v8::Context> context,
9494
v8::Local<v8::Object> params);
9595

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+
96103
private:
97104
NetworkInspector* inspector_;
98105
v8_inspector::V8Inspector* v8_inspector_;

src/inspector/node_protocol.pdl

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,16 @@ experimental domain Network
185185
boolean success
186186
optional IO.StreamHandle stream
187187

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+
188198
# Disables network tracking, prevents network events from being sent to the client.
189199
command disable
190200

@@ -285,6 +295,31 @@ experimental domain Network
285295
integer encodedDataLength
286296
# Data that was received.
287297
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
288323

289324
# Support for inspecting node process state.
290325
experimental domain NodeRuntime

test/common/websocket-server.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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;

0 commit comments

Comments
 (0)