Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to specify the masking key #1989

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions doc/ws.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,8 +462,9 @@ is a noop if the ready state is `CONNECTING` or `CLOSED`.

- `data` {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray} The
data to send in the ping frame.
- `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults to
`true` when `websocket` is not a server client.
- `mask` {Boolean|Buffer} Specifies whether `data` should be masked or not. If a
`Buffer` is provided, then its contents is used as the masking key. Defaults
to `true` when `websocket` is not a server client.
- `callback` {Function} An optional callback which is invoked when the ping
frame is written out. If an error occurs, the callback is called with the
error as its first argument.
Expand All @@ -474,8 +475,9 @@ Send a ping. This method throws an error if the ready state is `CONNECTING`.

- `data` {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray} The
data to send in the pong frame.
- `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults to
`true` when `websocket` is not a server client.
- `mask` {Boolean|Buffer} Specifies whether `data` should be masked or not. If a
`Buffer` is provided, then its contents is used as the masking key. Defaults
to `true` when `websocket` is not a server client.
- `callback` {Function} An optional callback which is invoked when the pong
frame is written out. If an error occurs, the callback is called with the
error as its first argument.
Expand Down Expand Up @@ -519,8 +521,9 @@ only removes listeners added with
Defaults to `true` when permessage-deflate is enabled.
- `fin` {Boolean} Specifies whether `data` is the last fragment of a message
or not. Defaults to `true`.
- `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults
to `true` when `websocket` is not a server client.
- `mask` {Boolean|Buffer} Specifies whether `data` should be masked or not. If
a `Buffer` is provided, then its contents is used as the masking key.
Defaults to `true` when `websocket` is not a server client.
- `callback` {Function} An optional callback which is invoked when `data` is
written out. If an error occurs, the callback is called with the error as its
first argument.
Expand Down
7 changes: 6 additions & 1 deletion lib/receiver.js
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,12 @@ class Receiver extends Writable {
}

data = this.consume(this._payloadLength);
if (this._masked) unmask(data, this._mask);
if (
this._masked &&
(this._mask[0] | this._mask[1] | this._mask[2] | this._mask[3]) !== 0
) {
unmask(data, this._mask);
}
}

if (this._opcode > 0x07) return this.controlMessage(data);
Expand Down
61 changes: 47 additions & 14 deletions lib/sender.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const { EMPTY_BUFFER } = require('./constants');
const { isValidStatusCode } = require('./validation');
const { mask: applyMask, toBuffer } = require('./buffer-util');

const mask = Buffer.alloc(4);
const _mask = Buffer.alloc(4);

/**
* HyBi Sender implementation.
Expand Down Expand Up @@ -42,8 +42,9 @@ class Sender {
* @param {Object} options Options object
* @param {Boolean} [options.fin=false] Specifies whether or not to set the
* FIN bit
* @param {Boolean} [options.mask=false] Specifies whether or not to mask
* `data`
* @param {(Boolean|Buffer)} [options.mask=false] Specifies whether or not to
* mask `data`. If a `Buffer` is provided, then its contents is used as
* the masking key
* @param {Number} options.opcode The opcode
* @param {Boolean} [options.readOnly=false] Specifies whether `data` can be
* modified
Expand Down Expand Up @@ -81,14 +82,24 @@ class Sender {

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

randomFillSync(mask, 0, 4);
let mask = _mask;

if (Buffer.isBuffer(options.mask)) {
mask = options.mask;
} else {
randomFillSync(_mask, 0, 4);
}

target[1] |= 0x80;
target[offset - 4] = mask[0];
target[offset - 3] = mask[1];
target[offset - 2] = mask[2];
target[offset - 1] = mask[3];

if ((mask[0] | mask[1] | mask[2] | mask[3]) === 0) {
return [target, data];
}

if (merge) {
applyMask(data, mask, target, offset, data.length);
return [target];
Expand All @@ -103,7 +114,9 @@ class Sender {
*
* @param {Number} [code] The status code component of the body
* @param {(String|Buffer)} [data] The message component of the body
* @param {Boolean} [mask=false] Specifies whether or not to mask the message
* @param {(Boolean|Buffer)} [mask=false] Specifies whether or not to mask the
* message. If a `Buffer` is provided, then its contents is used as the
* masking key
* @param {Function} [cb] Callback
* @public
*/
Expand Down Expand Up @@ -145,7 +158,9 @@ class Sender {
* Frames and sends a close message.
*
* @param {Buffer} data The message to send
* @param {Boolean} [mask=false] Specifies whether or not to mask `data`
* @param {(Boolean|Buffer)} [mask=false] Specifies whether or not to mask
* `data`. If a `Buffer` is provided, then its contents is used as the
* masking key
* @param {Function} [cb] Callback
* @private
*/
Expand All @@ -166,7 +181,9 @@ class Sender {
* Sends a ping message to the other peer.
*
* @param {*} data The message to send
* @param {Boolean} [mask=false] Specifies whether or not to mask `data`
* @param {(Boolean|Buffer)} [mask=false] Specifies whether or not to mask
* `data`. If a `Buffer` is provided, then its contents is used as the
* masking key
* @param {Function} [cb] Callback
* @public
*/
Expand All @@ -188,7 +205,9 @@ class Sender {
* Frames and sends a ping message.
*
* @param {Buffer} data The message to send
* @param {Boolean} [mask=false] Specifies whether or not to mask `data`
* @param {(Boolean|Buffer)} [mask=false] Specifies whether or not to mask
* `data`. If a `Buffer` is provided, then its contents is used as the
* masking key
* @param {Boolean} [readOnly=false] Specifies whether `data` can be modified
* @param {Function} [cb] Callback
* @private
Expand All @@ -210,7 +229,9 @@ class Sender {
* Sends a pong message to the other peer.
*
* @param {*} data The message to send
* @param {Boolean} [mask=false] Specifies whether or not to mask `data`
* @param {(Boolean|Buffer)} [mask=false] Specifies whether or not to mask
* `data`. If a `Buffer` is provided, then its contents is used as the
* masking key
* @param {Function} [cb] Callback
* @public
*/
Expand All @@ -232,7 +253,9 @@ class Sender {
* Frames and sends a pong message.
*
* @param {Buffer} data The message to send
* @param {Boolean} [mask=false] Specifies whether or not to mask `data`
* @param {(Boolean|Buffer)} [mask=false] Specifies whether or not to mask
* `data`. If a `Buffer` is provided, then its contents is used as the
* masking key
* @param {Boolean} [readOnly=false] Specifies whether `data` can be modified
* @param {Function} [cb] Callback
* @private
Expand Down Expand Up @@ -261,8 +284,9 @@ class Sender {
* compress `data`
* @param {Boolean} [options.fin=false] Specifies whether the fragment is the
* last one
* @param {Boolean} [options.mask=false] Specifies whether or not to mask
* `data`
* @param {(Boolean|Buffer)} [options.mask=false] Specifies whether or not to
* mask `data`. If a `Buffer` is provided, then its contents is used as
* the masking key
* @param {Function} [cb] Callback
* @public
*/
Expand Down Expand Up @@ -331,8 +355,9 @@ class Sender {
* @param {Number} options.opcode The opcode
* @param {Boolean} [options.fin=false] Specifies whether or not to set the
* FIN bit
* @param {Boolean} [options.mask=false] Specifies whether or not to mask
* `data`
* @param {(Boolean|Buffer)} [options.mask=false] Specifies whether or not to
* mask `data`. If a `Buffer` is provided, then its contents is used as
* the masking key
* @param {Boolean} [options.readOnly=false] Specifies whether `data` can be
* modified
* @param {Boolean} [options.rsv1=false] Specifies whether or not to set the
Expand All @@ -348,6 +373,7 @@ class Sender {

const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];

if (options.mask && options.mask.length) this._bufferedBytes += 4;
this._bufferedBytes += data.length;
this._deflating = true;
perMessageDeflate.compress(data, options.fin, (_, buf) => {
Expand All @@ -367,6 +393,7 @@ class Sender {
return;
}

if (options.mask && options.mask.length) this._bufferedBytes -= 4;
this._bufferedBytes -= data.length;
this._deflating = false;
options.readOnly = false;
Expand All @@ -383,7 +410,9 @@ class Sender {
dequeue() {
while (!this._deflating && this._queue.length) {
const params = this._queue.shift();
const mask = params[0] === this.dispatch ? params[3].mask : params[2];

if (mask && mask.length) this._bufferedBytes -= 4;
this._bufferedBytes -= params[1].length;
Reflect.apply(params[0], this, params.slice(1));
}
Expand All @@ -396,7 +425,11 @@ class Sender {
* @private
*/
enqueue(params) {
const mask = params[0] === this.dispatch ? params[3].mask : params[2];

if (mask && mask.length) this._bufferedBytes += 4;
this._bufferedBytes += params[1].length;

this._queue.push(params);
}

Expand Down
10 changes: 7 additions & 3 deletions lib/websocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,8 @@ class WebSocket extends EventEmitter {
* Send a ping.
*
* @param {*} [data] The data to send
* @param {Boolean} [mask] Indicates whether or not to mask `data`
* @param {(Boolean|Buffer)} [mask] Indicates whether or not to mask `data`.
* If a `Buffer` is provided, then its contents is used as the masking key
* @param {Function} [cb] Callback which is executed when the ping is sent
* @public
*/
Expand Down Expand Up @@ -373,7 +374,8 @@ class WebSocket extends EventEmitter {
* Send a pong.
*
* @param {*} [data] The data to send
* @param {Boolean} [mask] Indicates whether or not to mask `data`
* @param {(Boolean|Buffer)} [mask] Indicates whether or not to mask `data`.
* If a `Buffer` is provided, then its contents is used as the masking key
* @param {Function} [cb] Callback which is executed when the pong is sent
* @public
*/
Expand Down Expand Up @@ -429,7 +431,9 @@ class WebSocket extends EventEmitter {
* `data`
* @param {Boolean} [options.fin=true] Specifies whether the fragment is the
* last one
* @param {Boolean} [options.mask] Specifies whether or not to mask `data`
* @param {(Boolean|Buffer)} [options.mask] Specifies whether or not to mask
* `data`. If a `Buffer` is provided, then its contents is used as the
* masking key
* @param {Function} [cb] Callback which is executed when data is written out
* @public
*/
Expand Down
53 changes: 50 additions & 3 deletions test/websocket.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1299,10 +1299,11 @@ describe('WebSocket', () => {

it('can send a ping with data', (done) => {
const wss = new WebSocket.Server({ port: 0 }, () => {
const mask = Buffer.alloc(4);
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);

ws.on('open', () => {
ws.ping('hi', () => {
ws.ping('hi', mask, () => {
ws.ping('hi', true);
ws.close();
});
Expand Down Expand Up @@ -1468,10 +1469,11 @@ describe('WebSocket', () => {

it('can send a pong with data', (done) => {
const wss = new WebSocket.Server({ port: 0 }, () => {
const mask = Buffer.alloc(4);
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);

ws.on('open', () => {
ws.pong('hi', () => {
ws.pong('hi', mask, () => {
ws.pong('hi', true);
ws.close();
});
Expand Down Expand Up @@ -1877,7 +1879,7 @@ describe('WebSocket', () => {
});
});

it('honors the `mask` option', (done) => {
it('honors the `mask` option (1/2)', (done) => {
let clientCloseEventEmitted = false;
let serverClientCloseEventEmitted = false;

Expand Down Expand Up @@ -1921,6 +1923,51 @@ describe('WebSocket', () => {
});
});
});

it('honors the `mask` option (2/2)', (done) => {
const mask1 = Buffer.from('00000000', 'hex');
const mask2 = Buffer.from('00000001', 'hex');
const wss = new WebSocket.Server(
{ perMessageDeflate: true, port: 0 },
() => {
const ws = new WebSocket(`ws://localhost:${wss.address().port}`);

ws.on('open', () => {
ws.send('foo', { mask: mask1 });
ws.send('bar', { mask: mask2 });

assert.strictEqual(ws.bufferedAmount, 14);

ws.on('close', (code, reason) => {
assert.strictEqual(code, 1005);
assert.deepStrictEqual(reason, EMPTY_BUFFER);

wss.close(done);
});
});
}
);

wss.on('connection', (ws) => {
const chunks = [];

ws._socket.prependListener('data', (chunk) => {
chunks.push(chunk);
});

ws.on('message', (message) => {
if (message.toString() === 'foo') return;

const data = Buffer.concat(chunks);
const length = data[1] & 0x7f;

assert.ok(data.slice(2, 6).equals(mask1));
assert.ok(data.slice(length + 8, length + 12).equals(mask2));

ws.close();
});
});
});
});

describe('#close', () => {
Expand Down