Skip to content

Commit e628f2b

Browse files
committed
[feature] Support Windows named pipes (#2079)
Document how to connect to a named pipe endpoint and the limitations. Refs: #1808 Refs: #2075
1 parent 7ff26d9 commit e628f2b

File tree

4 files changed

+78
-71
lines changed

4 files changed

+78
-71
lines changed

doc/ws.md

+19-9
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
- [Class: WebSocket](#class-websocket)
1919
- [Ready state constants](#ready-state-constants)
2020
- [new WebSocket(address[, protocols][, options])](#new-websocketaddress-protocols-options)
21-
- [UNIX Domain Sockets](#unix-domain-sockets)
21+
- [IPC connections](#ipc-connections)
2222
- [Event: 'close'](#event-close-1)
2323
- [Event: 'error'](#event-error-1)
2424
- [Event: 'message'](#event-message)
@@ -323,17 +323,27 @@ context takeover.
323323

324324
Create a new WebSocket instance.
325325

326-
#### UNIX Domain Sockets
326+
#### IPC connections
327327

328-
`ws` supports making requests to UNIX domain sockets. To make one, use the
329-
following URL form:
328+
`ws` supports IPC connections. To connect to an IPC endpoint, use the following
329+
URL form:
330330

331-
```
332-
ws+unix:/absolute/path/to/uds_socket:/pathname?search_params
333-
```
331+
- On Unices
332+
333+
```
334+
ws+unix:/absolute/path/to/uds_socket:/pathname?search_params
335+
```
336+
337+
- On Windows
338+
339+
```
340+
ws+unix:\\.\pipe\pipe_name:/pathname?search_params
341+
```
334342

335-
The character `:` is the separator between the socket path and the URL path. If
336-
the URL path is omitted
343+
The character `:` is the separator between the IPC path (the Unix domain socket
344+
path or the Windows named pipe) and the URL path. The IPC path must not include
345+
the characters `:` and `?`, otherwise the URL is incorrectly parsed. If the URL
346+
path is omitted
337347

338348
```
339349
ws+unix:/absolute/path/to/uds_socket

lib/websocket.js

+9-9
Original file line numberDiff line numberDiff line change
@@ -677,13 +677,13 @@ function initAsClient(websocket, address, protocols, options) {
677677
}
678678

679679
const isSecure = parsedUrl.protocol === 'wss:';
680-
const isUnixSocket = parsedUrl.protocol === 'ws+unix:';
680+
const isIpcUrl = parsedUrl.protocol === 'ws+unix:';
681681
let invalidUrlMessage;
682682

683-
if (parsedUrl.protocol !== 'ws:' && !isSecure && !isUnixSocket) {
683+
if (parsedUrl.protocol !== 'ws:' && !isSecure && !isIpcUrl) {
684684
invalidUrlMessage =
685685
'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"';
686-
} else if (isUnixSocket && !parsedUrl.pathname) {
686+
} else if (isIpcUrl && !parsedUrl.pathname) {
687687
invalidUrlMessage = "The URL's pathname is empty";
688688
} else if (parsedUrl.hash) {
689689
invalidUrlMessage = 'The URL contains a fragment identifier';
@@ -760,7 +760,7 @@ function initAsClient(websocket, address, protocols, options) {
760760
opts.auth = `${parsedUrl.username}:${parsedUrl.password}`;
761761
}
762762

763-
if (isUnixSocket) {
763+
if (isIpcUrl) {
764764
const parts = opts.path.split(':');
765765

766766
opts.socketPath = parts[0];
@@ -771,9 +771,9 @@ function initAsClient(websocket, address, protocols, options) {
771771

772772
if (opts.followRedirects) {
773773
if (websocket._redirects === 0) {
774-
websocket._originalUnixSocket = isUnixSocket;
774+
websocket._originalIpc = isIpcUrl;
775775
websocket._originalSecure = isSecure;
776-
websocket._originalHostOrSocketPath = isUnixSocket
776+
websocket._originalHostOrSocketPath = isIpcUrl
777777
? opts.socketPath
778778
: parsedUrl.host;
779779

@@ -791,11 +791,11 @@ function initAsClient(websocket, address, protocols, options) {
791791
}
792792
}
793793
} else if (websocket.listenerCount('redirect') === 0) {
794-
const isSameHost = isUnixSocket
795-
? websocket._originalUnixSocket
794+
const isSameHost = isIpcUrl
795+
? websocket._originalIpc
796796
? opts.socketPath === websocket._originalHostOrSocketPath
797797
: false
798-
: websocket._originalUnixSocket
798+
: websocket._originalIpc
799799
? false
800800
: parsedUrl.host === websocket._originalHostOrSocketPath;
801801

test/websocket-server.test.js

+9-15
Original file line numberDiff line numberDiff line change
@@ -178,22 +178,16 @@ describe('WebSocketServer', () => {
178178
});
179179
});
180180

181-
it('uses a precreated http server listening on unix socket', function (done) {
182-
//
183-
// Skip this test on Windows. The URL parser:
184-
//
185-
// - Throws an error if the named pipe uses backward slashes.
186-
// - Incorrectly parses the path if the named pipe uses forward slashes.
187-
//
188-
if (process.platform === 'win32') return this.skip();
181+
it('uses a precreated http server listening on IPC', (done) => {
182+
const randomString = crypto.randomBytes(16).toString('hex');
183+
const ipcPath =
184+
process.platform === 'win32'
185+
? `\\\\.\\pipe\\ws-pipe-${randomString}`
186+
: path.join(os.tmpdir(), `ws-${randomString}.sock`);
189187

190188
const server = http.createServer();
191-
const sockPath = path.join(
192-
os.tmpdir(),
193-
`ws.${crypto.randomBytes(16).toString('hex')}.sock`
194-
);
195189

196-
server.listen(sockPath, () => {
190+
server.listen(ipcPath, () => {
197191
const wss = new WebSocket.Server({ server });
198192

199193
wss.on('connection', (ws, req) => {
@@ -210,8 +204,8 @@ describe('WebSocketServer', () => {
210204
}
211205
});
212206

213-
const ws = new WebSocket(`ws+unix:${sockPath}:/foo?bar=bar`);
214-
ws.on('open', () => new WebSocket(`ws+unix:${sockPath}`));
207+
const ws = new WebSocket(`ws+unix:${ipcPath}:/foo?bar=bar`);
208+
ws.on('open', () => new WebSocket(`ws+unix:${ipcPath}`));
215209
});
216210
});
217211
});

test/websocket.test.js

+41-38
Original file line numberDiff line numberDiff line change
@@ -1536,19 +1536,18 @@ describe('WebSocket', () => {
15361536
});
15371537
});
15381538

1539-
it('drops the Authorization, Cookie and Host headers (2/4)', function (done) {
1540-
if (process.platform === 'win32') return this.skip();
1541-
1539+
it('drops the Authorization, Cookie and Host headers (2/4)', (done) => {
15421540
// Test the `ws:` to `ws+unix:` case.
15431541

1544-
const socketPath = path.join(
1545-
os.tmpdir(),
1546-
`ws.${crypto.randomBytes(16).toString('hex')}.sock`
1547-
);
1542+
const randomString = crypto.randomBytes(16).toString('hex');
1543+
const ipcPath =
1544+
process.platform === 'win32'
1545+
? `\\\\.\\pipe\\ws-pipe-${randomString}`
1546+
: path.join(os.tmpdir(), `ws-${randomString}.sock`);
15481547

15491548
server.once('upgrade', (req, socket) => {
15501549
socket.end(
1551-
`HTTP/1.1 302 Found\r\nLocation: ws+unix:${socketPath}\r\n\r\n`
1550+
`HTTP/1.1 302 Found\r\nLocation: ws+unix:${ipcPath}\r\n\r\n`
15521551
);
15531552
});
15541553

@@ -1563,7 +1562,7 @@ describe('WebSocket', () => {
15631562
ws.close();
15641563
});
15651564

1566-
redirectedServer.listen(socketPath, () => {
1565+
redirectedServer.listen(ipcPath, () => {
15671566
const headers = {
15681567
authorization: 'Basic Zm9vOmJhcg==',
15691568
cookie: 'foo=bar',
@@ -1589,34 +1588,42 @@ describe('WebSocket', () => {
15891588

15901589
ws.on('close', (code) => {
15911590
assert.strictEqual(code, 1005);
1592-
assert.strictEqual(ws.url, `ws+unix:${socketPath}`);
1591+
assert.strictEqual(ws.url, `ws+unix:${ipcPath}`);
15931592
assert.strictEqual(ws._redirects, 1);
15941593

15951594
redirectedServer.close(done);
15961595
});
15971596
});
15981597
});
15991598

1600-
it('drops the Authorization, Cookie and Host headers (3/4)', function (done) {
1601-
if (process.platform === 'win32') return this.skip();
1602-
1599+
it('drops the Authorization, Cookie and Host headers (3/4)', (done) => {
16031600
// Test the `ws+unix:` to `ws+unix:` case.
16041601

1605-
const redirectingServerSocketPath = path.join(
1606-
os.tmpdir(),
1607-
`ws.${crypto.randomBytes(16).toString('hex')}.sock`
1608-
);
1609-
const redirectedServerSocketPath = path.join(
1610-
os.tmpdir(),
1611-
`ws.${crypto.randomBytes(16).toString('hex')}.sock`
1612-
);
1602+
const randomString1 = crypto.randomBytes(16).toString('hex');
1603+
const randomString2 = crypto.randomBytes(16).toString('hex');
1604+
let redirectingServerIpcPath;
1605+
let redirectedServerIpcPath;
1606+
1607+
if (process.platform === 'win32') {
1608+
redirectingServerIpcPath = `\\\\.\\pipe\\ws-pipe-${randomString1}`;
1609+
redirectedServerIpcPath = `\\\\.\\pipe\\ws-pipe-${randomString2}`;
1610+
} else {
1611+
redirectingServerIpcPath = path.join(
1612+
os.tmpdir(),
1613+
`ws-${randomString1}.sock`
1614+
);
1615+
redirectedServerIpcPath = path.join(
1616+
os.tmpdir(),
1617+
`ws-${randomString2}.sock`
1618+
);
1619+
}
16131620

16141621
const redirectingServer = http.createServer();
16151622

16161623
redirectingServer.on('upgrade', (req, socket) => {
16171624
socket.end(
16181625
'HTTP/1.1 302 Found\r\n' +
1619-
`Location: ws+unix:${redirectedServerSocketPath}\r\n\r\n`
1626+
`Location: ws+unix:${redirectedServerIpcPath}\r\n\r\n`
16201627
);
16211628
});
16221629

@@ -1631,8 +1638,8 @@ describe('WebSocket', () => {
16311638
ws.close();
16321639
});
16331640

1634-
redirectingServer.listen(redirectingServerSocketPath, listening);
1635-
redirectedServer.listen(redirectedServerSocketPath, listening);
1641+
redirectingServer.listen(redirectingServerIpcPath, listening);
1642+
redirectedServer.listen(redirectedServerIpcPath, listening);
16361643

16371644
let callCount = 0;
16381645

@@ -1645,7 +1652,7 @@ describe('WebSocket', () => {
16451652
host: 'foo'
16461653
};
16471654

1648-
const ws = new WebSocket(`ws+unix:${redirectingServerSocketPath}`, {
1655+
const ws = new WebSocket(`ws+unix:${redirectingServerIpcPath}`, {
16491656
followRedirects: true,
16501657
headers
16511658
});
@@ -1664,10 +1671,7 @@ describe('WebSocket', () => {
16641671

16651672
ws.on('close', (code) => {
16661673
assert.strictEqual(code, 1005);
1667-
assert.strictEqual(
1668-
ws.url,
1669-
`ws+unix:${redirectedServerSocketPath}`
1670-
);
1674+
assert.strictEqual(ws.url, `ws+unix:${redirectedServerIpcPath}`);
16711675
assert.strictEqual(ws._redirects, 1);
16721676

16731677
redirectingServer.close();
@@ -1676,9 +1680,7 @@ describe('WebSocket', () => {
16761680
}
16771681
});
16781682

1679-
it('drops the Authorization, Cookie and Host headers (4/4)', function (done) {
1680-
if (process.platform === 'win32') return this.skip();
1681-
1683+
it('drops the Authorization, Cookie and Host headers (4/4)', (done) => {
16821684
// Test the `ws+unix:` to `ws:` case.
16831685

16841686
const redirectingServer = http.createServer();
@@ -1696,12 +1698,13 @@ describe('WebSocket', () => {
16961698
ws.close();
16971699
});
16981700

1699-
const socketPath = path.join(
1700-
os.tmpdir(),
1701-
`ws.${crypto.randomBytes(16).toString('hex')}.sock`
1702-
);
1701+
const randomString = crypto.randomBytes(16).toString('hex');
1702+
const ipcPath =
1703+
process.platform === 'win32'
1704+
? `\\\\.\\pipe\\ws-pipe-${randomString}`
1705+
: path.join(os.tmpdir(), `ws-${randomString}.sock`);
17031706

1704-
redirectingServer.listen(socketPath, listening);
1707+
redirectingServer.listen(ipcPath, listening);
17051708
redirectedServer.listen(0, listening);
17061709

17071710
let callCount = 0;
@@ -1723,7 +1726,7 @@ describe('WebSocket', () => {
17231726
host: 'foo'
17241727
};
17251728

1726-
const ws = new WebSocket(`ws+unix:${socketPath}`, {
1729+
const ws = new WebSocket(`ws+unix:${ipcPath}`, {
17271730
followRedirects: true,
17281731
headers
17291732
});

0 commit comments

Comments
 (0)