Skip to content

Commit 5828624

Browse files
committed
inspector: initial support websocket inspection
Refs: #53946
1 parent 134625d commit 5828624

File tree

9 files changed

+413
-1
lines changed

9 files changed

+413
-1
lines changed

doc/api/inspector.md

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

605+
### `inspector.Network.webSocketCreated([params])`
606+
607+
<!-- YAML
608+
added:
609+
- REPLACEME
610+
-->
611+
612+
* `params` {Object}
613+
614+
This feature is only available with the `--experimental-network-inspection` flag enabled.
615+
616+
Broadcasts the `Network.webSocketCreated` event to connected frontends. This event indicates that
617+
a WebSocket connection has been initiated.
618+
619+
### `inspector.Network.webSocketHandshakeResponseReceived([params])`
620+
621+
<!-- YAML
622+
added:
623+
- REPLACEME
624+
-->
625+
626+
* `params` {Object}
627+
628+
This feature is only available with the `--experimental-network-inspection` flag enabled.
629+
630+
Broadcasts the `Network.webSocketHandshakeResponseReceived` event to connected frontends.
631+
This event indicates that the WebSocket handshake response has been received.
632+
633+
### `inspector.Network.webSocketClosed([params])`
634+
635+
<!-- YAML
636+
added:
637+
- REPLACEME
638+
-->
639+
640+
* `params` {Object}
641+
642+
This feature is only available with the `--experimental-network-inspection` flag enabled.
643+
644+
Broadcasts the `Network.webSocketClosed` event to connected frontends.
645+
This event indicates that a WebSocket connection has been closed.
646+
605647
### `inspector.NetworkResources.put`
606648

607649
<!-- 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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,35 @@ function onClientResponseFinish({ request }) {
206206
});
207207
}
208208

209+
function onWebSocketOpen({ websocket }) {
210+
websocket[kInspectorRequestId] = getNextRequestId();
211+
const url = websocket.url.toString();
212+
Network.webSocketCreated({
213+
requestId: websocket[kInspectorRequestId],
214+
url,
215+
});
216+
// TODO: Use handshake response data from undici diagnostics when available.
217+
Network.webSocketHandshakeResponseReceived({
218+
requestId: websocket[kInspectorRequestId],
219+
timestamp: getMonotonicTime(),
220+
response: {
221+
status: 101,
222+
statusText: 'Switching Protocols',
223+
headers: {},
224+
},
225+
});
226+
}
227+
228+
function onWebSocketClose({ websocket }) {
229+
if (typeof websocket[kInspectorRequestId] !== 'string') {
230+
return;
231+
}
232+
Network.webSocketClosed({
233+
requestId: websocket[kInspectorRequestId],
234+
timestamp: getMonotonicTime(),
235+
});
236+
}
237+
209238
function enable() {
210239
dc.subscribe('undici:request:create', onClientRequestStart);
211240
dc.subscribe('undici:request:error', onClientRequestError);
@@ -214,6 +243,8 @@ function enable() {
214243
dc.subscribe('undici:request:bodyChunkSent', onClientRequestBodyChunkSent);
215244
dc.subscribe('undici:request:bodySent', onClientRequestBodySent);
216245
dc.subscribe('undici:request:bodyChunkReceived', onClientRequestBodyChunkReceived);
246+
dc.subscribe('undici:websocket:open', onWebSocketOpen);
247+
dc.subscribe('undici:websocket:close', onWebSocketClose);
217248
}
218249

219250
function disable() {
@@ -224,6 +255,8 @@ function disable() {
224255
dc.unsubscribe('undici:request:bodyChunkSent', onClientRequestBodyChunkSent);
225256
dc.unsubscribe('undici:request:bodySent', onClientRequestBodySent);
226257
dc.unsubscribe('undici:request:bodyChunkReceived', onClientRequestBodyChunkReceived);
258+
dc.unsubscribe('undici:websocket:open', onWebSocketOpen);
259+
dc.unsubscribe('undici:websocket:close', onWebSocketClose);
227260
}
228261

229262
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: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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+
responseError = false,
12+
}) {
13+
this.port = port;
14+
this.server = http.createServer();
15+
this.clients = new Set();
16+
this.responseError = responseError;
17+
18+
this.server.on('upgrade', this.handleUpgrade.bind(this));
19+
}
20+
21+
start() {
22+
return new Promise((resolve) => {
23+
this.server.listen(this.port, () => {
24+
this.port = this.server.address().port;
25+
resolve();
26+
});
27+
}).catch((err) => {
28+
console.error('Failed to start WebSocket server:', err);
29+
});
30+
}
31+
32+
handleUpgrade(req, socket, head) {
33+
const key = req.headers['sec-websocket-key'];
34+
const acceptKey = this.generateAcceptValue(key);
35+
const responseHeaders = [
36+
'HTTP/1.1 101 Switching Protocols',
37+
'Upgrade: websocket',
38+
'Connection: Upgrade',
39+
`Sec-WebSocket-Accept: ${acceptKey}`,
40+
];
41+
42+
socket.write(responseHeaders.join('\r\n') + '\r\n\r\n');
43+
this.clients.add(socket);
44+
45+
socket.on('data', (buffer) => {
46+
const opcode = buffer[0] & 0x0f;
47+
48+
if (opcode === 0x8) {
49+
socket.end();
50+
this.clients.delete(socket);
51+
return;
52+
}
53+
54+
if (this.responseError) {
55+
socket.write(Buffer.from([0x88, 0x00])); // close frame
56+
socket.end();
57+
this.clients.delete(socket);
58+
return;
59+
}
60+
socket.write(this.encodeMessage('Hello from server!'));
61+
});
62+
63+
socket.on('close', () => {
64+
this.clients.delete(socket);
65+
});
66+
67+
socket.on('error', (err) => {
68+
console.error('Socket error:', err);
69+
this.clients.delete(socket);
70+
});
71+
}
72+
73+
generateAcceptValue(secWebSocketKey) {
74+
return crypto
75+
.createHash('sha1')
76+
.update(secWebSocketKey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary')
77+
.digest('base64');
78+
}
79+
80+
decodeMessage(buffer) {
81+
const secondByte = buffer[1];
82+
const length = secondByte & 127;
83+
const maskStart = 2;
84+
const dataStart = maskStart + 4;
85+
const masks = buffer.slice(maskStart, dataStart);
86+
const data = buffer.slice(dataStart, dataStart + length);
87+
const result = Buffer.alloc(length);
88+
89+
for (let i = 0; i < length; i++) {
90+
result[i] = data[i] ^ masks[i % 4];
91+
}
92+
93+
return result.toString();
94+
}
95+
96+
encodeMessage(message) {
97+
const msgBuffer = Buffer.from(message);
98+
const length = msgBuffer.length;
99+
const frame = [0x81];
100+
101+
if (length < 126) {
102+
frame.push(length);
103+
} else if (length < 65536) {
104+
frame.push(126, (length >> 8) & 0xff, length & 0xff);
105+
} else {
106+
throw new Error('Message too long');
107+
}
108+
109+
return Buffer.concat([Buffer.from(frame), msgBuffer]);
110+
}
111+
}
112+
113+
module.exports = WebSocketServer;

0 commit comments

Comments
 (0)