Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/node/webSocketTransport.ts
13394 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
// WebSocket transport for the sessions process protocol.
7
// Uses JSON serialization with URI revival for cross-process communication.
8
9
import { Emitter } from '../../../base/common/event.js';
10
import { Disposable } from '../../../base/common/lifecycle.js';
11
import { connectionTokenQueryName } from '../../../base/common/network.js';
12
import { ILogService } from '../../log/common/log.js';
13
import { JSON_RPC_PARSE_ERROR, type AhpServerNotification, type JsonRpcResponse, type ProtocolMessage } from '../common/state/sessionProtocol.js';
14
import type { IProtocolServer, IProtocolTransport } from '../common/state/sessionTransport.js';
15
import type * as wsTypes from 'ws';
16
import type * as httpTypes from 'http';
17
import type * as urlTypes from 'url';
18
19
/**
20
* Options for creating a {@link WebSocketProtocolServer}.
21
* Provide either `port`+`host` or `socketPath`, not both.
22
*/
23
export interface IWebSocketServerOptions {
24
/** TCP port to listen on. Ignored when {@link socketPath} is set. */
25
readonly port?: number;
26
/** Host/IP to bind to. Defaults to `'127.0.0.1'`. */
27
readonly host?: string;
28
/** Unix domain socket / Windows named pipe path. Takes precedence over port. */
29
readonly socketPath?: string;
30
/**
31
* Optional token validator. When provided, WebSocket upgrade requests
32
* must include a valid token in the `tkn` query parameter.
33
*/
34
readonly connectionTokenValidate?: (token: unknown) => boolean;
35
}
36
37
// ---- Per-connection transport -----------------------------------------------
38
39
/**
40
* Wraps a single WebSocket connection as an {@link IProtocolTransport}.
41
* Messages are serialized as JSON with URI revival.
42
*/
43
export class WebSocketProtocolTransport extends Disposable implements IProtocolTransport {
44
45
private readonly _onMessage = this._register(new Emitter<ProtocolMessage>());
46
readonly onMessage = this._onMessage.event;
47
48
private readonly _onClose = this._register(new Emitter<void>());
49
readonly onClose = this._onClose.event;
50
51
constructor(
52
private readonly _ws: wsTypes.WebSocket,
53
private readonly _WebSocket: typeof wsTypes.WebSocket,
54
) {
55
super();
56
57
this._ws.on('message', (data: Buffer | string) => {
58
try {
59
const text = typeof data === 'string' ? data : data.toString('utf-8');
60
const message = JSON.parse(text) as ProtocolMessage;
61
this._onMessage.fire(message);
62
} catch {
63
this.send({ jsonrpc: '2.0', id: null!, error: { code: JSON_RPC_PARSE_ERROR, message: 'Parse error' } });
64
}
65
});
66
67
this._ws.on('close', () => {
68
this._onClose.fire();
69
});
70
71
this._ws.on('error', () => {
72
// Error always precedes close — closing is handled in the close handler.
73
this._onClose.fire();
74
});
75
}
76
77
send(message: ProtocolMessage | AhpServerNotification | JsonRpcResponse): void {
78
if (this._ws.readyState === this._WebSocket.OPEN) {
79
this._ws.send(JSON.stringify(message));
80
}
81
}
82
83
override dispose(): void {
84
this._ws.close();
85
super.dispose();
86
}
87
}
88
89
// ---- Server -----------------------------------------------------------------
90
91
/**
92
* WebSocket server that accepts client connections and wraps each one
93
* as an {@link IProtocolTransport}.
94
*
95
* Use the static {@link create} method to construct — it dynamically imports
96
* `ws` and `http`/`url` so the modules are only loaded when needed.
97
*/
98
export class WebSocketProtocolServer extends Disposable implements IProtocolServer {
99
100
private readonly _wss: wsTypes.WebSocketServer;
101
private readonly _httpServer: httpTypes.Server | undefined;
102
private readonly _WebSocket: typeof wsTypes.WebSocket;
103
104
private readonly _onConnection = this._register(new Emitter<IProtocolTransport>());
105
readonly onConnection = this._onConnection.event;
106
107
get address(): string | undefined {
108
const addr = this._wss.address();
109
if (!addr || typeof addr === 'string') {
110
return addr ?? undefined;
111
}
112
return `${addr.address}:${addr.port}`;
113
}
114
115
/**
116
* Creates a new WebSocket protocol server. Dynamically imports `ws`,
117
* `http`, and `url` so callers don't pay the cost when unused.
118
*/
119
static async create(
120
options: IWebSocketServerOptions | number,
121
logService: ILogService,
122
): Promise<WebSocketProtocolServer> {
123
const [ws, http, url] = await Promise.all([
124
import('ws'),
125
import('http'),
126
import('url'),
127
]);
128
return new WebSocketProtocolServer(options, logService, ws, http, url);
129
}
130
131
private constructor(
132
options: IWebSocketServerOptions | number,
133
private readonly _logService: ILogService,
134
ws: typeof wsTypes,
135
http: typeof httpTypes,
136
url: typeof urlTypes,
137
) {
138
super();
139
140
this._WebSocket = ws.WebSocket;
141
142
// Backwards compat: accept a plain port number
143
const opts: IWebSocketServerOptions = typeof options === 'number' ? { port: options } : options;
144
const host = opts.host ?? '127.0.0.1';
145
146
const verifyClient = opts.connectionTokenValidate
147
? (info: { req: httpTypes.IncomingMessage }, cb: (res: boolean, code?: number, message?: string) => void) => {
148
const parsedUrl = url.parse(info.req.url ?? '', true);
149
const token = parsedUrl.query[connectionTokenQueryName];
150
if (!opts.connectionTokenValidate!(token)) {
151
this._logService.warn('[WebSocketProtocol] Connection rejected: invalid connection token');
152
cb(false, 403, 'Forbidden');
153
return;
154
}
155
cb(true);
156
}
157
: undefined;
158
159
if (opts.socketPath) {
160
// For socket paths, create an HTTP server listening on the path
161
// and attach the WebSocket server to it.
162
this._httpServer = http.createServer();
163
this._wss = new ws.WebSocketServer({ server: this._httpServer, verifyClient });
164
this._httpServer.listen(opts.socketPath, () => {
165
this._logService.info(`[WebSocketProtocol] Server listening on socket ${opts.socketPath}`);
166
});
167
} else {
168
this._wss = new ws.WebSocketServer({ port: opts.port, host, verifyClient });
169
this._logService.info(`[WebSocketProtocol] Server listening on ${host}:${opts.port}`);
170
}
171
172
this._wss.on('connection', (wsConn) => {
173
this._logService.trace('[WebSocketProtocol] New client connection');
174
const transport = new WebSocketProtocolTransport(wsConn, this._WebSocket);
175
this._onConnection.fire(transport);
176
});
177
178
this._wss.on('error', (err) => {
179
this._logService.error('[WebSocketProtocol] Server error', err);
180
});
181
}
182
183
override dispose(): void {
184
this._wss.close();
185
this._httpServer?.close();
186
super.dispose();
187
}
188
}
189
190