From e628f2bdb6277fa36e8607cf1c3fbaf713f7bcef Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Thu, 22 Sep 2022 20:37:20 +0200 Subject: [PATCH] [feature] Support Windows named pipes (#2079) Document how to connect to a named pipe endpoint and the limitations. Refs: https://github.com/websockets/ws/pull/1808 Refs: https://github.com/websockets/ws/pull/2075 --- doc/ws.md | 28 +++++++++---- lib/websocket.js | 18 ++++---- test/websocket-server.test.js | 24 ++++------- test/websocket.test.js | 79 ++++++++++++++++++----------------- 4 files changed, 78 insertions(+), 71 deletions(-) diff --git a/doc/ws.md b/doc/ws.md index ca608fde2..e045a0db1 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -18,7 +18,7 @@ - [Class: WebSocket](#class-websocket) - [Ready state constants](#ready-state-constants) - [new WebSocket(address[, protocols][, options])](#new-websocketaddress-protocols-options) - - [UNIX Domain Sockets](#unix-domain-sockets) + - [IPC connections](#ipc-connections) - [Event: 'close'](#event-close-1) - [Event: 'error'](#event-error-1) - [Event: 'message'](#event-message) @@ -323,17 +323,27 @@ context takeover. Create a new WebSocket instance. -#### UNIX Domain Sockets +#### IPC connections -`ws` supports making requests to UNIX domain sockets. To make one, use the -following URL form: +`ws` supports IPC connections. To connect to an IPC endpoint, use the following +URL form: -``` -ws+unix:/absolute/path/to/uds_socket:/pathname?search_params -``` +- On Unices + + ``` + ws+unix:/absolute/path/to/uds_socket:/pathname?search_params + ``` + +- On Windows + + ``` + ws+unix:\\.\pipe\pipe_name:/pathname?search_params + ``` -The character `:` is the separator between the socket path and the URL path. If -the URL path is omitted +The character `:` is the separator between the IPC path (the Unix domain socket +path or the Windows named pipe) and the URL path. The IPC path must not include +the characters `:` and `?`, otherwise the URL is incorrectly parsed. If the URL +path is omitted ``` ws+unix:/absolute/path/to/uds_socket diff --git a/lib/websocket.js b/lib/websocket.js index 8c4512d48..4391c73ab 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -677,13 +677,13 @@ function initAsClient(websocket, address, protocols, options) { } const isSecure = parsedUrl.protocol === 'wss:'; - const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; + const isIpcUrl = parsedUrl.protocol === 'ws+unix:'; let invalidUrlMessage; - if (parsedUrl.protocol !== 'ws:' && !isSecure && !isUnixSocket) { + if (parsedUrl.protocol !== 'ws:' && !isSecure && !isIpcUrl) { invalidUrlMessage = 'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"'; - } else if (isUnixSocket && !parsedUrl.pathname) { + } else if (isIpcUrl && !parsedUrl.pathname) { invalidUrlMessage = "The URL's pathname is empty"; } else if (parsedUrl.hash) { invalidUrlMessage = 'The URL contains a fragment identifier'; @@ -760,7 +760,7 @@ function initAsClient(websocket, address, protocols, options) { opts.auth = `${parsedUrl.username}:${parsedUrl.password}`; } - if (isUnixSocket) { + if (isIpcUrl) { const parts = opts.path.split(':'); opts.socketPath = parts[0]; @@ -771,9 +771,9 @@ function initAsClient(websocket, address, protocols, options) { if (opts.followRedirects) { if (websocket._redirects === 0) { - websocket._originalUnixSocket = isUnixSocket; + websocket._originalIpc = isIpcUrl; websocket._originalSecure = isSecure; - websocket._originalHostOrSocketPath = isUnixSocket + websocket._originalHostOrSocketPath = isIpcUrl ? opts.socketPath : parsedUrl.host; @@ -791,11 +791,11 @@ function initAsClient(websocket, address, protocols, options) { } } } else if (websocket.listenerCount('redirect') === 0) { - const isSameHost = isUnixSocket - ? websocket._originalUnixSocket + const isSameHost = isIpcUrl + ? websocket._originalIpc ? opts.socketPath === websocket._originalHostOrSocketPath : false - : websocket._originalUnixSocket + : websocket._originalIpc ? false : parsedUrl.host === websocket._originalHostOrSocketPath; diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 2ea81d319..abed1650a 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -178,22 +178,16 @@ describe('WebSocketServer', () => { }); }); - it('uses a precreated http server listening on unix socket', function (done) { - // - // Skip this test on Windows. The URL parser: - // - // - Throws an error if the named pipe uses backward slashes. - // - Incorrectly parses the path if the named pipe uses forward slashes. - // - if (process.platform === 'win32') return this.skip(); + it('uses a precreated http server listening on IPC', (done) => { + const randomString = crypto.randomBytes(16).toString('hex'); + const ipcPath = + process.platform === 'win32' + ? `\\\\.\\pipe\\ws-pipe-${randomString}` + : path.join(os.tmpdir(), `ws-${randomString}.sock`); const server = http.createServer(); - const sockPath = path.join( - os.tmpdir(), - `ws.${crypto.randomBytes(16).toString('hex')}.sock` - ); - server.listen(sockPath, () => { + server.listen(ipcPath, () => { const wss = new WebSocket.Server({ server }); wss.on('connection', (ws, req) => { @@ -210,8 +204,8 @@ describe('WebSocketServer', () => { } }); - const ws = new WebSocket(`ws+unix:${sockPath}:/foo?bar=bar`); - ws.on('open', () => new WebSocket(`ws+unix:${sockPath}`)); + const ws = new WebSocket(`ws+unix:${ipcPath}:/foo?bar=bar`); + ws.on('open', () => new WebSocket(`ws+unix:${ipcPath}`)); }); }); }); diff --git a/test/websocket.test.js b/test/websocket.test.js index cf4cf10f2..fad80ddb9 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -1536,19 +1536,18 @@ describe('WebSocket', () => { }); }); - it('drops the Authorization, Cookie and Host headers (2/4)', function (done) { - if (process.platform === 'win32') return this.skip(); - + it('drops the Authorization, Cookie and Host headers (2/4)', (done) => { // Test the `ws:` to `ws+unix:` case. - const socketPath = path.join( - os.tmpdir(), - `ws.${crypto.randomBytes(16).toString('hex')}.sock` - ); + const randomString = crypto.randomBytes(16).toString('hex'); + const ipcPath = + process.platform === 'win32' + ? `\\\\.\\pipe\\ws-pipe-${randomString}` + : path.join(os.tmpdir(), `ws-${randomString}.sock`); server.once('upgrade', (req, socket) => { socket.end( - `HTTP/1.1 302 Found\r\nLocation: ws+unix:${socketPath}\r\n\r\n` + `HTTP/1.1 302 Found\r\nLocation: ws+unix:${ipcPath}\r\n\r\n` ); }); @@ -1563,7 +1562,7 @@ describe('WebSocket', () => { ws.close(); }); - redirectedServer.listen(socketPath, () => { + redirectedServer.listen(ipcPath, () => { const headers = { authorization: 'Basic Zm9vOmJhcg==', cookie: 'foo=bar', @@ -1589,7 +1588,7 @@ describe('WebSocket', () => { ws.on('close', (code) => { assert.strictEqual(code, 1005); - assert.strictEqual(ws.url, `ws+unix:${socketPath}`); + assert.strictEqual(ws.url, `ws+unix:${ipcPath}`); assert.strictEqual(ws._redirects, 1); redirectedServer.close(done); @@ -1597,26 +1596,34 @@ describe('WebSocket', () => { }); }); - it('drops the Authorization, Cookie and Host headers (3/4)', function (done) { - if (process.platform === 'win32') return this.skip(); - + it('drops the Authorization, Cookie and Host headers (3/4)', (done) => { // Test the `ws+unix:` to `ws+unix:` case. - const redirectingServerSocketPath = path.join( - os.tmpdir(), - `ws.${crypto.randomBytes(16).toString('hex')}.sock` - ); - const redirectedServerSocketPath = path.join( - os.tmpdir(), - `ws.${crypto.randomBytes(16).toString('hex')}.sock` - ); + const randomString1 = crypto.randomBytes(16).toString('hex'); + const randomString2 = crypto.randomBytes(16).toString('hex'); + let redirectingServerIpcPath; + let redirectedServerIpcPath; + + if (process.platform === 'win32') { + redirectingServerIpcPath = `\\\\.\\pipe\\ws-pipe-${randomString1}`; + redirectedServerIpcPath = `\\\\.\\pipe\\ws-pipe-${randomString2}`; + } else { + redirectingServerIpcPath = path.join( + os.tmpdir(), + `ws-${randomString1}.sock` + ); + redirectedServerIpcPath = path.join( + os.tmpdir(), + `ws-${randomString2}.sock` + ); + } const redirectingServer = http.createServer(); redirectingServer.on('upgrade', (req, socket) => { socket.end( 'HTTP/1.1 302 Found\r\n' + - `Location: ws+unix:${redirectedServerSocketPath}\r\n\r\n` + `Location: ws+unix:${redirectedServerIpcPath}\r\n\r\n` ); }); @@ -1631,8 +1638,8 @@ describe('WebSocket', () => { ws.close(); }); - redirectingServer.listen(redirectingServerSocketPath, listening); - redirectedServer.listen(redirectedServerSocketPath, listening); + redirectingServer.listen(redirectingServerIpcPath, listening); + redirectedServer.listen(redirectedServerIpcPath, listening); let callCount = 0; @@ -1645,7 +1652,7 @@ describe('WebSocket', () => { host: 'foo' }; - const ws = new WebSocket(`ws+unix:${redirectingServerSocketPath}`, { + const ws = new WebSocket(`ws+unix:${redirectingServerIpcPath}`, { followRedirects: true, headers }); @@ -1664,10 +1671,7 @@ describe('WebSocket', () => { ws.on('close', (code) => { assert.strictEqual(code, 1005); - assert.strictEqual( - ws.url, - `ws+unix:${redirectedServerSocketPath}` - ); + assert.strictEqual(ws.url, `ws+unix:${redirectedServerIpcPath}`); assert.strictEqual(ws._redirects, 1); redirectingServer.close(); @@ -1676,9 +1680,7 @@ describe('WebSocket', () => { } }); - it('drops the Authorization, Cookie and Host headers (4/4)', function (done) { - if (process.platform === 'win32') return this.skip(); - + it('drops the Authorization, Cookie and Host headers (4/4)', (done) => { // Test the `ws+unix:` to `ws:` case. const redirectingServer = http.createServer(); @@ -1696,12 +1698,13 @@ describe('WebSocket', () => { ws.close(); }); - const socketPath = path.join( - os.tmpdir(), - `ws.${crypto.randomBytes(16).toString('hex')}.sock` - ); + const randomString = crypto.randomBytes(16).toString('hex'); + const ipcPath = + process.platform === 'win32' + ? `\\\\.\\pipe\\ws-pipe-${randomString}` + : path.join(os.tmpdir(), `ws-${randomString}.sock`); - redirectingServer.listen(socketPath, listening); + redirectingServer.listen(ipcPath, listening); redirectedServer.listen(0, listening); let callCount = 0; @@ -1723,7 +1726,7 @@ describe('WebSocket', () => { host: 'foo' }; - const ws = new WebSocket(`ws+unix:${socketPath}`, { + const ws = new WebSocket(`ws+unix:${ipcPath}`, { followRedirects: true, headers });