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;
111 } else if (typeof code !== 'number' || !validation.isValidStatusCode(code)) {
112 throw new TypeError('First argument must be a valid error code number');
113 } else if (data === undefined || data === '') {
114 buf = Buffer.allocUnsafe(2);
115 buf.writeUInt16BE(code, 0);
117 buf = Buffer.allocUnsafe(2 + Buffer.byteLength(data));
118 buf.writeUInt16BE(code, 0);
122 if (this._deflating) {
123 this.enqueue([this.doClose, buf, mask, cb]);
125 this.doClose(buf, mask, cb);
130 * Frames and sends a close message.
132 * @param {Buffer} data The message to send
133 * @param {Boolean} mask Specifies whether or not to mask `data`
134 * @param {Function} cb Callback
137 doClose (data, mask, cb) {
138 this.sendFrame(Sender.frame(data, {
148 * Sends a ping message to the other peer.
150 * @param {*} data The message to send
151 * @param {Boolean} mask Specifies whether or not to mask `data`
152 * @param {Function} cb Callback
155 ping (data, mask, cb) {
158 if (!Buffer.isBuffer(data)) {
159 if (data instanceof ArrayBuffer) {
160 data = Buffer.from(data);
161 } else if (ArrayBuffer.isView(data)) {
162 data = viewToBuffer(data);
164 data = Buffer.from(data);
169 if (this._deflating) {
170 this.enqueue([this.doPing, data, mask, readOnly, cb]);
172 this.doPing(data, mask, readOnly, cb);
177 * Frames and sends a ping message.
179 * @param {*} data The message to send
180 * @param {Boolean} mask Specifies whether or not to mask `data`
181 * @param {Boolean} readOnly Specifies whether `data` can be modified
182 * @param {Function} cb Callback
185 doPing (data, mask, readOnly, cb) {
186 this.sendFrame(Sender.frame(data, {
196 * Sends a pong message to the other peer.
198 * @param {*} data The message to send
199 * @param {Boolean} mask Specifies whether or not to mask `data`
200 * @param {Function} cb Callback
203 pong (data, mask, cb) {
206 if (!Buffer.isBuffer(data)) {
207 if (data instanceof ArrayBuffer) {
208 data = Buffer.from(data);
209 } else if (ArrayBuffer.isView(data)) {
210 data = viewToBuffer(data);
212 data = Buffer.from(data);
217 if (this._deflating) {
218 this.enqueue([this.doPong, data, mask, readOnly, cb]);
220 this.doPong(data, mask, readOnly, cb);
225 * Frames and sends a pong message.
227 * @param {*} data The message to send
228 * @param {Boolean} mask Specifies whether or not to mask `data`
229 * @param {Boolean} readOnly Specifies whether `data` can be modified
230 * @param {Function} cb Callback
233 doPong (data, mask, readOnly, cb) {
234 this.sendFrame(Sender.frame(data, {
244 * Sends a data message to the other peer.
246 * @param {*} data The message to send
247 * @param {Object} options Options object
248 * @param {Boolean} options.compress Specifies whether or not to compress `data`
249 * @param {Boolean} options.binary Specifies whether `data` is binary or text
250 * @param {Boolean} options.fin Specifies whether the fragment is the last one
251 * @param {Boolean} options.mask Specifies whether or not to mask `data`
252 * @param {Function} cb Callback
255 send (data, options, cb) {
256 var opcode = options.binary ? 2 : 1;
257 var rsv1 = options.compress;
260 if (!Buffer.isBuffer(data)) {
261 if (data instanceof ArrayBuffer) {
262 data = Buffer.from(data);
263 } else if (ArrayBuffer.isView(data)) {
264 data = viewToBuffer(data);
266 data = Buffer.from(data);
271 const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
273 if (this._firstFragment) {
274 this._firstFragment = false;
275 if (rsv1 && perMessageDeflate) {
276 rsv1 = data.length >= perMessageDeflate._threshold;
278 this._compress = rsv1;
284 if (options.fin) this._firstFragment = true;
286 if (perMessageDeflate) {
295 if (this._deflating) {
296 this.enqueue([this.dispatch, data, this._compress, opts, cb]);
298 this.dispatch(data, this._compress, opts, cb);
301 this.sendFrame(Sender.frame(data, {
312 * Dispatches a data message.
314 * @param {Buffer} data The message to send
315 * @param {Boolean} compress Specifies whether or not to compress `data`
316 * @param {Object} options Options object
317 * @param {Number} options.opcode The opcode
318 * @param {Boolean} options.readOnly Specifies whether `data` can be modified
319 * @param {Boolean} options.fin Specifies whether or not to set the FIN bit
320 * @param {Boolean} options.mask Specifies whether or not to mask `data`
321 * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit
322 * @param {Function} cb Callback
325 dispatch (data, compress, options, cb) {
327 this.sendFrame(Sender.frame(data, options), cb);
331 const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
333 this._deflating = true;
334 perMessageDeflate.compress(data, options.fin, (_, buf) => {
335 options.readOnly = false;
336 this.sendFrame(Sender.frame(buf, options), cb);
337 this._deflating = false;
343 * Executes queued send operations.
348 while (!this._deflating && this._queue.length) {
349 const params = this._queue.shift();
351 this._bufferedBytes -= params[1].length;
352 params[0].apply(this, params.slice(1));
357 * Enqueues a send operation.
359 * @param {Array} params Send operation parameters.
363 this._bufferedBytes += params[1].length;
364 this._queue.push(params);
370 * @param {Buffer[]} list The frame to send
371 * @param {Function} cb Callback
374 sendFrame (list, cb) {
375 if (list.length === 2) {
376 this._socket.write(list[0]);
377 this._socket.write(list[1], cb);
379 this._socket.write(list[0], cb);
384 module.exports = Sender;
387 * Converts an `ArrayBuffer` view into a buffer.
389 * @param {(DataView|TypedArray)} view The view to convert
390 * @return {Buffer} Converted view
393 function viewToBuffer (view) {
394 const buf = Buffer.from(view.buffer);
396 if (view.byteLength !== view.buffer.byteLength) {
397 return buf.slice(view.byteOffset, view.byteOffset + view.byteLength);