Path: blob/main/src/vs/platform/agentHost/node/webSocketTransport.ts
13394 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45// WebSocket transport for the sessions process protocol.6// Uses JSON serialization with URI revival for cross-process communication.78import { Emitter } from '../../../base/common/event.js';9import { Disposable } from '../../../base/common/lifecycle.js';10import { connectionTokenQueryName } from '../../../base/common/network.js';11import { ILogService } from '../../log/common/log.js';12import { JSON_RPC_PARSE_ERROR, type AhpServerNotification, type JsonRpcResponse, type ProtocolMessage } from '../common/state/sessionProtocol.js';13import type { IProtocolServer, IProtocolTransport } from '../common/state/sessionTransport.js';14import type * as wsTypes from 'ws';15import type * as httpTypes from 'http';16import type * as urlTypes from 'url';1718/**19* Options for creating a {@link WebSocketProtocolServer}.20* Provide either `port`+`host` or `socketPath`, not both.21*/22export interface IWebSocketServerOptions {23/** TCP port to listen on. Ignored when {@link socketPath} is set. */24readonly port?: number;25/** Host/IP to bind to. Defaults to `'127.0.0.1'`. */26readonly host?: string;27/** Unix domain socket / Windows named pipe path. Takes precedence over port. */28readonly socketPath?: string;29/**30* Optional token validator. When provided, WebSocket upgrade requests31* must include a valid token in the `tkn` query parameter.32*/33readonly connectionTokenValidate?: (token: unknown) => boolean;34}3536// ---- Per-connection transport -----------------------------------------------3738/**39* Wraps a single WebSocket connection as an {@link IProtocolTransport}.40* Messages are serialized as JSON with URI revival.41*/42export class WebSocketProtocolTransport extends Disposable implements IProtocolTransport {4344private readonly _onMessage = this._register(new Emitter<ProtocolMessage>());45readonly onMessage = this._onMessage.event;4647private readonly _onClose = this._register(new Emitter<void>());48readonly onClose = this._onClose.event;4950constructor(51private readonly _ws: wsTypes.WebSocket,52private readonly _WebSocket: typeof wsTypes.WebSocket,53) {54super();5556this._ws.on('message', (data: Buffer | string) => {57try {58const text = typeof data === 'string' ? data : data.toString('utf-8');59const message = JSON.parse(text) as ProtocolMessage;60this._onMessage.fire(message);61} catch {62this.send({ jsonrpc: '2.0', id: null!, error: { code: JSON_RPC_PARSE_ERROR, message: 'Parse error' } });63}64});6566this._ws.on('close', () => {67this._onClose.fire();68});6970this._ws.on('error', () => {71// Error always precedes close — closing is handled in the close handler.72this._onClose.fire();73});74}7576send(message: ProtocolMessage | AhpServerNotification | JsonRpcResponse): void {77if (this._ws.readyState === this._WebSocket.OPEN) {78this._ws.send(JSON.stringify(message));79}80}8182override dispose(): void {83this._ws.close();84super.dispose();85}86}8788// ---- Server -----------------------------------------------------------------8990/**91* WebSocket server that accepts client connections and wraps each one92* as an {@link IProtocolTransport}.93*94* Use the static {@link create} method to construct — it dynamically imports95* `ws` and `http`/`url` so the modules are only loaded when needed.96*/97export class WebSocketProtocolServer extends Disposable implements IProtocolServer {9899private readonly _wss: wsTypes.WebSocketServer;100private readonly _httpServer: httpTypes.Server | undefined;101private readonly _WebSocket: typeof wsTypes.WebSocket;102103private readonly _onConnection = this._register(new Emitter<IProtocolTransport>());104readonly onConnection = this._onConnection.event;105106get address(): string | undefined {107const addr = this._wss.address();108if (!addr || typeof addr === 'string') {109return addr ?? undefined;110}111return `${addr.address}:${addr.port}`;112}113114/**115* Creates a new WebSocket protocol server. Dynamically imports `ws`,116* `http`, and `url` so callers don't pay the cost when unused.117*/118static async create(119options: IWebSocketServerOptions | number,120logService: ILogService,121): Promise<WebSocketProtocolServer> {122const [ws, http, url] = await Promise.all([123import('ws'),124import('http'),125import('url'),126]);127return new WebSocketProtocolServer(options, logService, ws, http, url);128}129130private constructor(131options: IWebSocketServerOptions | number,132private readonly _logService: ILogService,133ws: typeof wsTypes,134http: typeof httpTypes,135url: typeof urlTypes,136) {137super();138139this._WebSocket = ws.WebSocket;140141// Backwards compat: accept a plain port number142const opts: IWebSocketServerOptions = typeof options === 'number' ? { port: options } : options;143const host = opts.host ?? '127.0.0.1';144145const verifyClient = opts.connectionTokenValidate146? (info: { req: httpTypes.IncomingMessage }, cb: (res: boolean, code?: number, message?: string) => void) => {147const parsedUrl = url.parse(info.req.url ?? '', true);148const token = parsedUrl.query[connectionTokenQueryName];149if (!opts.connectionTokenValidate!(token)) {150this._logService.warn('[WebSocketProtocol] Connection rejected: invalid connection token');151cb(false, 403, 'Forbidden');152return;153}154cb(true);155}156: undefined;157158if (opts.socketPath) {159// For socket paths, create an HTTP server listening on the path160// and attach the WebSocket server to it.161this._httpServer = http.createServer();162this._wss = new ws.WebSocketServer({ server: this._httpServer, verifyClient });163this._httpServer.listen(opts.socketPath, () => {164this._logService.info(`[WebSocketProtocol] Server listening on socket ${opts.socketPath}`);165});166} else {167this._wss = new ws.WebSocketServer({ port: opts.port, host, verifyClient });168this._logService.info(`[WebSocketProtocol] Server listening on ${host}:${opts.port}`);169}170171this._wss.on('connection', (wsConn) => {172this._logService.trace('[WebSocketProtocol] New client connection');173const transport = new WebSocketProtocolTransport(wsConn, this._WebSocket);174this._onConnection.fire(transport);175});176177this._wss.on('error', (err) => {178this._logService.error('[WebSocketProtocol] Server error', err);179});180}181182override dispose(): void {183this._wss.close();184this._httpServer?.close();185super.dispose();186}187}188189190