Skip to content

Commit

Permalink
[feature] Support Windows named pipes (#2079)
Browse files Browse the repository at this point in the history
Document how to connect to a named pipe endpoint and the limitations.

Refs: #1808
Refs: #2075
  • Loading branch information
lpinca committed Sep 22, 2022
1 parent 7ff26d9 commit e628f2b
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 71 deletions.
28 changes: 19 additions & 9 deletions doc/ws.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
18 changes: 9 additions & 9 deletions lib/websocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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];
Expand All @@ -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;

Expand All @@ -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;

Expand Down
24 changes: 9 additions & 15 deletions test/websocket-server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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}`));
});
});
});
Expand Down
79 changes: 41 additions & 38 deletions test/websocket.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`
);
});

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

redirectedServer.listen(socketPath, () => {
redirectedServer.listen(ipcPath, () => {
const headers = {
authorization: 'Basic Zm9vOmJhcg==',
cookie: 'foo=bar',
Expand All @@ -1589,34 +1588,42 @@ 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);
});
});
});

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`
);
});

Expand All @@ -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;

Expand All @@ -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
});
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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;
Expand All @@ -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
});
Expand Down

0 comments on commit e628f2b

Please sign in to comment.