UNPKG

10.6 kBJavaScriptView Raw
1'use strict';
2
3const crypto = require('crypto');
4
5const PerMessageDeflate = require('./permessage-deflate');
6const bufferUtil = require('./buffer-util');
7const validation = require('./validation');
8const constants = require('./constants');
9
10/**
11 * HyBi Sender implementation.
12 */
13class Sender {
14 /**
15 * Creates a Sender instance.
16 *
17 * @param {net.Socket} socket The connection socket
18 * @param {Object} extensions An object containing the negotiated extensions
19 */
20 constructor (socket, extensions) {
21 this._extensions = extensions || {};
22 this._socket = socket;
23
24 this._firstFragment = true;
25 this._compress = false;
26
27 this._bufferedBytes = 0;
28 this._deflating = false;
29 this._queue = [];
30 }
31
32 /**
33 * Frames a piece of data according to the HyBi WebSocket protocol.
34 *
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
43 * @public
44 */
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;
49
50 if (data.length >= 65536) {
51 offset += 8;
52 payloadLength = 127;
53 } else if (data.length > 125) {
54 offset += 2;
55 payloadLength = 126;
56 }
57
58 const target = Buffer.allocUnsafe(merge ? data.length + offset : offset);
59
60 target[0] = options.fin ? options.opcode | 0x80 : options.opcode;
61 if (options.rsv1) target[0] |= 0x40;
62
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);
68 }
69
70 if (!options.mask) {
71 target[1] = payloadLength;
72 if (merge) {
73 data.copy(target, offset);
74 return [target];
75 }
76
77 return [target, data];
78 }
79
80 const mask = crypto.randomBytes(4);
81
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];
87
88 if (merge) {
89 bufferUtil.mask(data, mask, target, offset, data.length);
90 return [target];
91 }
92
93 bufferUtil.mask(data, mask, data, 0, data.length);
94 return [target, data];
95 }
96
97 /**
98 * Sends a close message to the other peer.
99 *
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
104 * @public
105 */
106 close (code, data, mask, cb) {
107 var buf;
108
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);
116 } else {
117 buf = Buffer.allocUnsafe(2 + Buffer.byteLength(data));
118 buf.writeUInt16BE(code, 0);
119 buf.write(data, 2);
120 }
121
122 if (this._deflating) {
123 this.enqueue([this.doClose, buf, mask, cb]);
124 } else {
125 this.doClose(buf, mask, cb);
126 }
127 }
128
129 /**
130 * Frames and sends a close message.
131 *
132 * @param {Buffer} data The message to send
133 * @param {Boolean} mask Specifies whether or not to mask `data`
134 * @param {Function} cb Callback
135 * @private
136 */
137 doClose (data, mask, cb) {
138 this.sendFrame(Sender.frame(data, {
139 fin: true,
140 rsv1: false,
141 opcode: 0x08,
142 mask,
143 readOnly: false
144 }), cb);
145 }
146
147 /**
148 * Sends a ping message to the other peer.
149 *
150 * @param {*} data The message to send
151 * @param {Boolean} mask Specifies whether or not to mask `data`
152 * @param {Function} cb Callback
153 * @public
154 */
155 ping (data, mask, cb) {
156 var readOnly = true;
157
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);
163 } else {
164 data = Buffer.from(data);
165 readOnly = false;
166 }
167 }
168
169 if (this._deflating) {
170 this.enqueue([this.doPing, data, mask, readOnly, cb]);
171 } else {
172 this.doPing(data, mask, readOnly, cb);
173 }
174 }
175
176 /**
177 * Frames and sends a ping message.
178 *
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
183 * @private
184 */
185 doPing (data, mask, readOnly, cb) {
186 this.sendFrame(Sender.frame(data, {
187 fin: true,
188 rsv1: false,
189 opcode: 0x09,
190 mask,
191 readOnly
192 }), cb);
193 }
194
195 /**
196 * Sends a pong message to the other peer.
197 *
198 * @param {*} data The message to send
199 * @param {Boolean} mask Specifies whether or not to mask `data`
200 * @param {Function} cb Callback
201 * @public
202 */
203 pong (data, mask, cb) {
204 var readOnly = true;
205
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);
211 } else {
212 data = Buffer.from(data);
213 readOnly = false;
214 }
215 }
216
217 if (this._deflating) {
218 this.enqueue([this.doPong, data, mask, readOnly, cb]);
219 } else {
220 this.doPong(data, mask, readOnly, cb);
221 }
222 }
223
224 /**
225 * Frames and sends a pong message.
226 *
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
231 * @private
232 */
233 doPong (data, mask, readOnly, cb) {
234 this.sendFrame(Sender.frame(data, {
235 fin: true,
236 rsv1: false,
237 opcode: 0x0a,
238 mask,
239 readOnly
240 }), cb);
241 }
242
243 /**
244 * Sends a data message to the other peer.
245 *
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
253 * @public
254 */
255 send (data, options, cb) {
256 var opcode = options.binary ? 2 : 1;
257 var rsv1 = options.compress;
258 var readOnly = true;
259
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);
265 } else {
266 data = Buffer.from(data);
267 readOnly = false;
268 }
269 }
270
271 const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
272
273 if (this._firstFragment) {
274 this._firstFragment = false;
275 if (rsv1 && perMessageDeflate) {
276 rsv1 = data.length >= perMessageDeflate._threshold;
277 }
278 this._compress = rsv1;
279 } else {
280 rsv1 = false;
281 opcode = 0;
282 }
283
284 if (options.fin) this._firstFragment = true;
285
286 if (perMessageDeflate) {
287 const opts = {
288 fin: options.fin,
289 rsv1,
290 opcode,
291 mask: options.mask,
292 readOnly
293 };
294
295 if (this._deflating) {
296 this.enqueue([this.dispatch, data, this._compress, opts, cb]);
297 } else {
298 this.dispatch(data, this._compress, opts, cb);
299 }
300 } else {
301 this.sendFrame(Sender.frame(data, {
302 fin: options.fin,
303 rsv1: false,
304 opcode,
305 mask: options.mask,
306 readOnly
307 }), cb);
308 }
309 }
310
311 /**
312 * Dispatches a data message.
313 *
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
323 * @private
324 */
325 dispatch (data, compress, options, cb) {
326 if (!compress) {
327 this.sendFrame(Sender.frame(data, options), cb);
328 return;
329 }
330
331 const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
332
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;
338 this.dequeue();
339 });
340 }
341
342 /**
343 * Executes queued send operations.
344 *
345 * @private
346 */
347 dequeue () {
348 while (!this._deflating && this._queue.length) {
349 const params = this._queue.shift();
350
351 this._bufferedBytes -= params[1].length;
352 params[0].apply(this, params.slice(1));
353 }
354 }
355
356 /**
357 * Enqueues a send operation.
358 *
359 * @param {Array} params Send operation parameters.
360 * @private
361 */
362 enqueue (params) {
363 this._bufferedBytes += params[1].length;
364 this._queue.push(params);
365 }
366
367 /**
368 * Sends a frame.
369 *
370 * @param {Buffer[]} list The frame to send
371 * @param {Function} cb Callback
372 * @private
373 */
374 sendFrame (list, cb) {
375 if (list.length === 2) {
376 this._socket.write(list[0]);
377 this._socket.write(list[1], cb);
378 } else {
379 this._socket.write(list[0], cb);
380 }
381 }
382}
383
384module.exports = Sender;
385
386/**
387 * Converts an `ArrayBuffer` view into a buffer.
388 *
389 * @param {(DataView|TypedArray)} view The view to convert
390 * @return {Buffer} Converted view
391 * @private
392 */
393function viewToBuffer (view) {
394 const buf = Buffer.from(view.buffer);
395
396 if (view.byteLength !== view.buffer.byteLength) {
397 return buf.slice(view.byteOffset, view.byteOffset + view.byteLength);
398 }
399
400 return buf;
401}