Skip to content

Commit 348eefb

Browse files
committed
[feature] Introduce the generateMask option
The `generateMask` option specifies a function that can be used to generate custom masking keys. Refs: #1986 Refs: #1988 Refs: #1989
1 parent c82b087 commit 348eefb

File tree

4 files changed

+67
-5
lines changed

4 files changed

+67
-5
lines changed

doc/ws.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,9 @@ This class represents a WebSocket. It extends the `EventEmitter`.
270270
- `options` {Object}
271271
- `followRedirects` {Boolean} Whether or not to follow redirects. Defaults to
272272
`false`.
273+
- `generateMask` {Function} The function used to generate the making key. It
274+
must return a `Buffer` of length 4 synchronously. By default the masking key
275+
is generated with cryptographically strong random bytes.
273276
- `handshakeTimeout` {Number} Timeout in milliseconds for the handshake
274277
request. This is reset after every redirection.
275278
- `maxPayload` {Number} The maximum allowed message size in bytes.

lib/sender.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const { EMPTY_BUFFER } = require('./constants');
1111
const { isValidStatusCode } = require('./validation');
1212
const { mask: applyMask, toBuffer } = require('./buffer-util');
1313

14-
const mask = Buffer.alloc(4);
14+
const _mask = Buffer.alloc(4);
1515

1616
/**
1717
* HyBi Sender implementation.
@@ -22,9 +22,12 @@ class Sender {
2222
*
2323
* @param {(net.Socket|tls.Socket)} socket The connection socket
2424
* @param {Object} [extensions] An object containing the negotiated extensions
25+
* @param {Function} [generateMask] The function used to generate the masking
26+
* key
2527
*/
26-
constructor(socket, extensions) {
28+
constructor(socket, extensions, generateMask) {
2729
this._extensions = extensions || {};
30+
this._generateMask = generateMask;
2831
this._socket = socket;
2932

3033
this._firstFragment = true;
@@ -42,6 +45,8 @@ class Sender {
4245
* @param {Object} options Options object
4346
* @param {Boolean} [options.fin=false] Specifies whether or not to set the
4447
* FIN bit
48+
* @param {Function} [options.generateMask] The function used to generate the
49+
* masking key
4550
* @param {Boolean} [options.mask=false] Specifies whether or not to mask
4651
* `data`
4752
* @param {Number} options.opcode The opcode
@@ -81,7 +86,13 @@ class Sender {
8186

8287
if (!options.mask) return [target, data];
8388

84-
randomFillSync(mask, 0, 4);
89+
let mask = _mask;
90+
91+
if (options.generateMask) {
92+
mask = options.generateMask();
93+
} else {
94+
randomFillSync(_mask, 0, 4);
95+
}
8596

8697
target[1] |= 0x80;
8798
target[offset - 4] = mask[0];
@@ -156,6 +167,7 @@ class Sender {
156167
rsv1: false,
157168
opcode: 0x08,
158169
mask,
170+
generateMask: this._generateMask,
159171
readOnly: false
160172
}),
161173
cb
@@ -200,6 +212,7 @@ class Sender {
200212
rsv1: false,
201213
opcode: 0x09,
202214
mask,
215+
generateMask: this.generateMask,
203216
readOnly
204217
}),
205218
cb
@@ -244,6 +257,7 @@ class Sender {
244257
rsv1: false,
245258
opcode: 0x0a,
246259
mask,
260+
generateMask: this._generateMask,
247261
readOnly
248262
}),
249263
cb
@@ -299,6 +313,7 @@ class Sender {
299313
rsv1,
300314
opcode,
301315
mask: options.mask,
316+
generateMask: this._generateMask,
302317
readOnly: toBuffer.readOnly
303318
};
304319

@@ -314,6 +329,7 @@ class Sender {
314329
rsv1: false,
315330
opcode,
316331
mask: options.mask,
332+
generateMask: this._generateMask,
317333
readOnly: toBuffer.readOnly
318334
}),
319335
cb
@@ -331,6 +347,8 @@ class Sender {
331347
* @param {Number} options.opcode The opcode
332348
* @param {Boolean} [options.fin=false] Specifies whether or not to set the
333349
* FIN bit
350+
* @param {Function} [options.generateMask] The function used to generate the
351+
* masking key
334352
* @param {Boolean} [options.mask=false] Specifies whether or not to mask
335353
* `data`
336354
* @param {Boolean} [options.readOnly=false] Specifies whether `data` can be

lib/websocket.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const https = require('https');
77
const http = require('http');
88
const net = require('net');
99
const tls = require('tls');
10-
const { randomBytes, createHash } = require('crypto');
10+
const { createHash, randomBytes } = require('crypto');
1111
const { Readable } = require('stream');
1212
const { URL } = require('url');
1313

@@ -192,6 +192,8 @@ class WebSocket extends EventEmitter {
192192
* server and client
193193
* @param {Buffer} head The first packet of the upgraded stream
194194
* @param {Object} options Options object
195+
* @param {Function} [options.generateMask] The function used to generate the
196+
* masking key
195197
* @param {Number} [options.maxPayload=0] The maximum allowed message size
196198
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
197199
* not to skip UTF-8 validation for text and close messages
@@ -206,7 +208,7 @@ class WebSocket extends EventEmitter {
206208
skipUTF8Validation: options.skipUTF8Validation
207209
});
208210

209-
this._sender = new Sender(socket, this._extensions);
211+
this._sender = new Sender(socket, this._extensions, options.generateMask);
210212
this._receiver = receiver;
211213
this._socket = socket;
212214

@@ -613,6 +615,8 @@ module.exports = WebSocket;
613615
* @param {Object} [options] Connection options
614616
* @param {Boolean} [options.followRedirects=false] Whether or not to follow
615617
* redirects
618+
* @param {Function} [options.generateMask] The function used to generate the
619+
* masking key
616620
* @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the
617621
* handshake request
618622
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
@@ -899,6 +903,7 @@ function initAsClient(websocket, address, protocols, options) {
899903
}
900904

901905
websocket.setSocket(socket, head, {
906+
generateMask: opts.generateMask,
902907
maxPayload: opts.maxPayload,
903908
skipUTF8Validation: opts.skipUTF8Validation
904909
});

test/websocket.test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,42 @@ describe('WebSocket', () => {
126126
/^RangeError: Unsupported protocol version: 1000 \(supported versions: 8, 13\)$/
127127
);
128128
});
129+
130+
it('honors the `generateMask` option', (done) => {
131+
const mask = Buffer.alloc(4);
132+
const wss = new WebSocket.Server({ port: 0 }, () => {
133+
const ws = new WebSocket(`ws://localhost:${wss.address().port}`, {
134+
generateMask() {
135+
return mask;
136+
}
137+
});
138+
139+
ws.on('open', () => {
140+
ws.send('foo');
141+
});
142+
143+
ws.on('close', (code, reason) => {
144+
assert.strictEqual(code, 1005);
145+
assert.deepStrictEqual(reason, EMPTY_BUFFER);
146+
147+
wss.close(done);
148+
});
149+
});
150+
151+
wss.on('connection', (ws) => {
152+
const chunks = [];
153+
154+
ws._socket.prependListener('data', (chunk) => {
155+
chunks.push(chunk);
156+
});
157+
158+
ws.on('message', () => {
159+
assert.ok(Buffer.concat(chunks).slice(2, 6).equals(mask));
160+
161+
ws.close();
162+
});
163+
});
164+
});
129165
});
130166
});
131167

0 commit comments

Comments
 (0)