3 const crypto = require('crypto');
5 const PerMessageDeflate = require('./permessage-deflate');
6 const bufferUtil = require('./buffer-util');
7 const validation = require('./validation');
8 const constants = require('./constants');
11 * HyBi Sender implementation.
15 * Creates a Sender instance.
17 * @param {net.Socket} socket The connection socket
18 * @param {Object} extensions An object containing the negotiated extensions
20 constructor(socket, extensions) {
21 this._extensions = extensions || {};
22 this._socket = socket;
24 this._firstFragment = true;
25 this._compress = false;
27 this._bufferedBytes = 0;
28 this._deflating = false;
33 * Frames a piece of data according to the HyBi WebSocket protocol.
35 * @param {Buffer} data The data to frame
36 * @param {Object} options Options object
37 * @param {Number} options.opcode The opcode
38 * @param {Boolean} options.readOnly Specifies whether `data` can be modified
39 * @param {Boolean} options.fin Specifies whether or not to set the FIN bit
40 * @param {Boolean} options.mask Specifies whether or not to mask `data`
41 * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit
42 * @return {Buffer[]} The framed data as a list of `Buffer` instances
45 static frame(data, options) {
46 const merge = data.length < 1024 || (options.mask && options.readOnly);
47 var offset = options.mask ? 6 : 2;
48 var payloadLength = data.length;
50 if (data.length >= 65536) {
53 } else if (data.length > 125) {
58 const target = Buffer.allocUnsafe(merge ? data.length + offset : offset);
60 target[0] = options.fin ? options.opcode | 0x80 : options.opcode;
61 if (options.rsv1) target[0] |= 0x40;
63 if (payloadLength === 126) {
64 target.writeUInt16BE(data.length, 2);
65 } else if (payloadLength === 127) {
66 target.writeUInt32BE(0, 2);
67 target.writeUInt32BE(data.length, 6);
71 target[1] = payloadLength;
73 data.copy(target, offset);
77 return [target, data];
80 const mask = crypto.randomBytes(4);
82 target[1] = payloadLength | 0x80;
83 target[offset - 4] = mask[0];
84 target[offset - 3] = mask[1];
85 target[offset - 2] = mask[2];
86 target[offset - 1] = mask[3];
89 bufferUtil.mask(data, mask, target, offset, data.length);
93 bufferUtil.mask(data, mask, data, 0, data.length);
94 return [target, data];
98 * Sends a close message to the other peer.
100 * @param {(Number|undefined)} code The status code component of the body
101 * @param {String} data The message component of the body
102 * @param {Boolean} mask Specifies whether or not to mask the message
103 * @param {Function} cb Callback
106 close(code, data, mask, cb) {
109 if (code === undefined) {
110 buf = constants.EMPTY_BUFFER;
112 typeof code !== 'number' ||
113 !validation.isValidStatusCode(code)
115 throw new TypeError('First argument must be a valid error code number');
116 } else if (data === undefined || data === '') {
117 buf = Buffer.allocUnsafe(2);
118 buf.writeUInt16BE(code, 0);
120 buf = Buffer.allocUnsafe(2 + Buffer.byteLength(data));
121 buf.writeUInt16BE(code, 0);
125 if (this._deflating) {
126 this.enqueue([this.doClose, buf, mask, cb]);
128 this.doClose(buf, mask, cb);
133 * Frames and sends a close message.
135 * @param {Buffer} data The message to send
136 * @param {Boolean} mask Specifies whether or not to mask `data`
137 * @param {Function} cb Callback
140 doClose(data, mask, cb) {
154 * Sends a ping message to the other peer.
156 * @param {*} data The message to send
157 * @param {Boolean} mask Specifies whether or not to mask `data`
158 * @param {Function} cb Callback
161 ping(data, mask, cb) {
164 if (!Buffer.isBuffer(data)) {
165 if (data instanceof ArrayBuffer) {
166 data = Buffer.from(data);
167 } else if (ArrayBuffer.isView(data)) {
168 data = viewToBuffer(data);
170 data = Buffer.from(data);
175 if (this._deflating) {
176 this.enqueue([this.doPing, data, mask, readOnly, cb]);
178 this.doPing(data, mask, readOnly, cb);
183 * Frames and sends a ping message.
185 * @param {*} data The message to send
186 * @param {Boolean} mask Specifies whether or not to mask `data`
187 * @param {Boolean} readOnly Specifies whether `data` can be modified
188 * @param {Function} cb Callback
191 doPing(data, mask, readOnly, cb) {
205 * Sends a pong message to the other peer.
207 * @param {*} data The message to send
208 * @param {Boolean} mask Specifies whether or not to mask `data`
209 * @param {Function} cb Callback
212 pong(data, mask, cb) {
215 if (!Buffer.isBuffer(data)) {
216 if (data instanceof ArrayBuffer) {
217 data = Buffer.from(data);
218 } else if (ArrayBuffer.isView(data)) {
219 data = viewToBuffer(data);
221 data = Buffer.from(data);
226 if (this._deflating) {
227 this.enqueue([this.doPong, data, mask, readOnly, cb]);
229 this.doPong(data, mask, readOnly, cb);
234 * Frames and sends a pong message.
236 * @param {*} data The message to send
237 * @param {Boolean} mask Specifies whether or not to mask `data`
238 * @param {Boolean} readOnly Specifies whether `data` can be modified
239 * @param {Function} cb Callback
242 doPong(data, mask, readOnly, cb) {
256 * Sends a data message to the other peer.
258 * @param {*} data The message to send
259 * @param {Object} options Options object
260 * @param {Boolean} options.compress Specifies whether or not to compress `data`
261 * @param {Boolean} options.binary Specifies whether `data` is binary or text
262 * @param {Boolean} options.fin Specifies whether the fragment is the last one
263 * @param {Boolean} options.mask Specifies whether or not to mask `data`
264 * @param {Function} cb Callback
267 send(data, options, cb) {
268 var opcode = options.binary ? 2 : 1;
269 var rsv1 = options.compress;
272 if (!Buffer.isBuffer(data)) {
273 if (data instanceof ArrayBuffer) {
274 data = Buffer.from(data);
275 } else if (ArrayBuffer.isView(data)) {
276 data = viewToBuffer(data);
278 data = Buffer.from(data);
283 const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
285 if (this._firstFragment) {
286 this._firstFragment = false;
287 if (rsv1 && perMessageDeflate) {
288 rsv1 = data.length >= perMessageDeflate._threshold;
290 this._compress = rsv1;
296 if (options.fin) this._firstFragment = true;
298 if (perMessageDeflate) {
307 if (this._deflating) {
308 this.enqueue([this.dispatch, data, this._compress, opts, cb]);
310 this.dispatch(data, this._compress, opts, cb);
327 * Dispatches a data message.
329 * @param {Buffer} data The message to send
330 * @param {Boolean} compress Specifies whether or not to compress `data`
331 * @param {Object} options Options object
332 * @param {Number} options.opcode The opcode
333 * @param {Boolean} options.readOnly Specifies whether `data` can be modified
334 * @param {Boolean} options.fin Specifies whether or not to set the FIN bit
335 * @param {Boolean} options.mask Specifies whether or not to mask `data`
336 * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit
337 * @param {Function} cb Callback
340 dispatch(data, compress, options, cb) {
342 this.sendFrame(Sender.frame(data, options), cb);
346 const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
348 this._deflating = true;
349 perMessageDeflate.compress(data, options.fin, (_, buf) => {
350 this._deflating = false;
351 options.readOnly = false;
352 this.sendFrame(Sender.frame(buf, options), cb);
358 * Executes queued send operations.
363 while (!this._deflating && this._queue.length) {
364 const params = this._queue.shift();
366 this._bufferedBytes -= params[1].length;
367 params[0].apply(this, params.slice(1));
372 * Enqueues a send operation.
374 * @param {Array} params Send operation parameters.
378 this._bufferedBytes += params[1].length;
379 this._queue.push(params);
385 * @param {Buffer[]} list The frame to send
386 * @param {Function} cb Callback
389 sendFrame(list, cb) {
390 if (list.length === 2) {
391 this._socket.write(list[0]);
392 this._socket.write(list[1], cb);
394 this._socket.write(list[0], cb);
399 module.exports = Sender;
402 * Converts an `ArrayBuffer` view into a buffer.
404 * @param {(DataView|TypedArray)} view The view to convert
405 * @return {Buffer} Converted view
408 function viewToBuffer(view) {
409 const buf = Buffer.from(view.buffer);
411 if (view.byteLength !== view.buffer.byteLength) {
412 return buf.slice(view.byteOffset, view.byteOffset + view.byteLength);