619 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			619 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | 'use strict'; | ||
|  | 
 | ||
|  | const { Writable } = require('stream'); | ||
|  | 
 | ||
|  | const PerMessageDeflate = require('./permessage-deflate'); | ||
|  | const { | ||
|  |   BINARY_TYPES, | ||
|  |   EMPTY_BUFFER, | ||
|  |   kStatusCode, | ||
|  |   kWebSocket | ||
|  | } = require('./constants'); | ||
|  | const { concat, toArrayBuffer, unmask } = require('./buffer-util'); | ||
|  | const { isValidStatusCode, isValidUTF8 } = require('./validation'); | ||
|  | 
 | ||
|  | const GET_INFO = 0; | ||
|  | const GET_PAYLOAD_LENGTH_16 = 1; | ||
|  | const GET_PAYLOAD_LENGTH_64 = 2; | ||
|  | const GET_MASK = 3; | ||
|  | const GET_DATA = 4; | ||
|  | const INFLATING = 5; | ||
|  | 
 | ||
|  | /** | ||
|  |  * HyBi Receiver implementation. | ||
|  |  * | ||
|  |  * @extends Writable | ||
|  |  */ | ||
|  | class Receiver extends Writable { | ||
|  |   /** | ||
|  |    * Creates a Receiver instance. | ||
|  |    * | ||
|  |    * @param {Object} [options] Options object | ||
|  |    * @param {String} [options.binaryType=nodebuffer] The type for binary data | ||
|  |    * @param {Object} [options.extensions] An object containing the negotiated | ||
|  |    *     extensions | ||
|  |    * @param {Boolean} [options.isServer=false] Specifies whether to operate in | ||
|  |    *     client or server mode | ||
|  |    * @param {Number} [options.maxPayload=0] The maximum allowed message length | ||
|  |    * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or | ||
|  |    *     not to skip UTF-8 validation for text and close messages | ||
|  |    */ | ||
|  |   constructor(options = {}) { | ||
|  |     super(); | ||
|  | 
 | ||
|  |     this._binaryType = options.binaryType || BINARY_TYPES[0]; | ||
|  |     this._extensions = options.extensions || {}; | ||
|  |     this._isServer = !!options.isServer; | ||
|  |     this._maxPayload = options.maxPayload | 0; | ||
|  |     this._skipUTF8Validation = !!options.skipUTF8Validation; | ||
|  |     this[kWebSocket] = undefined; | ||
|  | 
 | ||
|  |     this._bufferedBytes = 0; | ||
|  |     this._buffers = []; | ||
|  | 
 | ||
|  |     this._compressed = false; | ||
|  |     this._payloadLength = 0; | ||
|  |     this._mask = undefined; | ||
|  |     this._fragmented = 0; | ||
|  |     this._masked = false; | ||
|  |     this._fin = false; | ||
|  |     this._opcode = 0; | ||
|  | 
 | ||
|  |     this._totalPayloadLength = 0; | ||
|  |     this._messageLength = 0; | ||
|  |     this._fragments = []; | ||
|  | 
 | ||
|  |     this._state = GET_INFO; | ||
|  |     this._loop = false; | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Implements `Writable.prototype._write()`. | ||
|  |    * | ||
|  |    * @param {Buffer} chunk The chunk of data to write | ||
|  |    * @param {String} encoding The character encoding of `chunk` | ||
|  |    * @param {Function} cb Callback | ||
|  |    * @private | ||
|  |    */ | ||
|  |   _write(chunk, encoding, cb) { | ||
|  |     if (this._opcode === 0x08 && this._state == GET_INFO) return cb(); | ||
|  | 
 | ||
|  |     this._bufferedBytes += chunk.length; | ||
|  |     this._buffers.push(chunk); | ||
|  |     this.startLoop(cb); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Consumes `n` bytes from the buffered data. | ||
|  |    * | ||
|  |    * @param {Number} n The number of bytes to consume | ||
|  |    * @return {Buffer} The consumed bytes | ||
|  |    * @private | ||
|  |    */ | ||
|  |   consume(n) { | ||
|  |     this._bufferedBytes -= n; | ||
|  | 
 | ||
|  |     if (n === this._buffers[0].length) return this._buffers.shift(); | ||
|  | 
 | ||
|  |     if (n < this._buffers[0].length) { | ||
|  |       const buf = this._buffers[0]; | ||
|  |       this._buffers[0] = buf.slice(n); | ||
|  |       return buf.slice(0, n); | ||
|  |     } | ||
|  | 
 | ||
|  |     const dst = Buffer.allocUnsafe(n); | ||
|  | 
 | ||
|  |     do { | ||
|  |       const buf = this._buffers[0]; | ||
|  |       const offset = dst.length - n; | ||
|  | 
 | ||
|  |       if (n >= buf.length) { | ||
|  |         dst.set(this._buffers.shift(), offset); | ||
|  |       } else { | ||
|  |         dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset); | ||
|  |         this._buffers[0] = buf.slice(n); | ||
|  |       } | ||
|  | 
 | ||
|  |       n -= buf.length; | ||
|  |     } while (n > 0); | ||
|  | 
 | ||
|  |     return dst; | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Starts the parsing loop. | ||
|  |    * | ||
|  |    * @param {Function} cb Callback | ||
|  |    * @private | ||
|  |    */ | ||
|  |   startLoop(cb) { | ||
|  |     let err; | ||
|  |     this._loop = true; | ||
|  | 
 | ||
|  |     do { | ||
|  |       switch (this._state) { | ||
|  |         case GET_INFO: | ||
|  |           err = this.getInfo(); | ||
|  |           break; | ||
|  |         case GET_PAYLOAD_LENGTH_16: | ||
|  |           err = this.getPayloadLength16(); | ||
|  |           break; | ||
|  |         case GET_PAYLOAD_LENGTH_64: | ||
|  |           err = this.getPayloadLength64(); | ||
|  |           break; | ||
|  |         case GET_MASK: | ||
|  |           this.getMask(); | ||
|  |           break; | ||
|  |         case GET_DATA: | ||
|  |           err = this.getData(cb); | ||
|  |           break; | ||
|  |         default: | ||
|  |           // `INFLATING`
 | ||
|  |           this._loop = false; | ||
|  |           return; | ||
|  |       } | ||
|  |     } while (this._loop); | ||
|  | 
 | ||
|  |     cb(err); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Reads the first two bytes of a frame. | ||
|  |    * | ||
|  |    * @return {(RangeError|undefined)} A possible error | ||
|  |    * @private | ||
|  |    */ | ||
|  |   getInfo() { | ||
|  |     if (this._bufferedBytes < 2) { | ||
|  |       this._loop = false; | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     const buf = this.consume(2); | ||
|  | 
 | ||
|  |     if ((buf[0] & 0x30) !== 0x00) { | ||
|  |       this._loop = false; | ||
|  |       return error( | ||
|  |         RangeError, | ||
|  |         'RSV2 and RSV3 must be clear', | ||
|  |         true, | ||
|  |         1002, | ||
|  |         'WS_ERR_UNEXPECTED_RSV_2_3' | ||
|  |       ); | ||
|  |     } | ||
|  | 
 | ||
|  |     const compressed = (buf[0] & 0x40) === 0x40; | ||
|  | 
 | ||
|  |     if (compressed && !this._extensions[PerMessageDeflate.extensionName]) { | ||
|  |       this._loop = false; | ||
|  |       return error( | ||
|  |         RangeError, | ||
|  |         'RSV1 must be clear', | ||
|  |         true, | ||
|  |         1002, | ||
|  |         'WS_ERR_UNEXPECTED_RSV_1' | ||
|  |       ); | ||
|  |     } | ||
|  | 
 | ||
|  |     this._fin = (buf[0] & 0x80) === 0x80; | ||
|  |     this._opcode = buf[0] & 0x0f; | ||
|  |     this._payloadLength = buf[1] & 0x7f; | ||
|  | 
 | ||
|  |     if (this._opcode === 0x00) { | ||
|  |       if (compressed) { | ||
|  |         this._loop = false; | ||
|  |         return error( | ||
|  |           RangeError, | ||
|  |           'RSV1 must be clear', | ||
|  |           true, | ||
|  |           1002, | ||
|  |           'WS_ERR_UNEXPECTED_RSV_1' | ||
|  |         ); | ||
|  |       } | ||
|  | 
 | ||
|  |       if (!this._fragmented) { | ||
|  |         this._loop = false; | ||
|  |         return error( | ||
|  |           RangeError, | ||
|  |           'invalid opcode 0', | ||
|  |           true, | ||
|  |           1002, | ||
|  |           'WS_ERR_INVALID_OPCODE' | ||
|  |         ); | ||
|  |       } | ||
|  | 
 | ||
|  |       this._opcode = this._fragmented; | ||
|  |     } else if (this._opcode === 0x01 || this._opcode === 0x02) { | ||
|  |       if (this._fragmented) { | ||
|  |         this._loop = false; | ||
|  |         return error( | ||
|  |           RangeError, | ||
|  |           `invalid opcode ${this._opcode}`, | ||
|  |           true, | ||
|  |           1002, | ||
|  |           'WS_ERR_INVALID_OPCODE' | ||
|  |         ); | ||
|  |       } | ||
|  | 
 | ||
|  |       this._compressed = compressed; | ||
|  |     } else if (this._opcode > 0x07 && this._opcode < 0x0b) { | ||
|  |       if (!this._fin) { | ||
|  |         this._loop = false; | ||
|  |         return error( | ||
|  |           RangeError, | ||
|  |           'FIN must be set', | ||
|  |           true, | ||
|  |           1002, | ||
|  |           'WS_ERR_EXPECTED_FIN' | ||
|  |         ); | ||
|  |       } | ||
|  | 
 | ||
|  |       if (compressed) { | ||
|  |         this._loop = false; | ||
|  |         return error( | ||
|  |           RangeError, | ||
|  |           'RSV1 must be clear', | ||
|  |           true, | ||
|  |           1002, | ||
|  |           'WS_ERR_UNEXPECTED_RSV_1' | ||
|  |         ); | ||
|  |       } | ||
|  | 
 | ||
|  |       if (this._payloadLength > 0x7d) { | ||
|  |         this._loop = false; | ||
|  |         return error( | ||
|  |           RangeError, | ||
|  |           `invalid payload length ${this._payloadLength}`, | ||
|  |           true, | ||
|  |           1002, | ||
|  |           'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' | ||
|  |         ); | ||
|  |       } | ||
|  |     } else { | ||
|  |       this._loop = false; | ||
|  |       return error( | ||
|  |         RangeError, | ||
|  |         `invalid opcode ${this._opcode}`, | ||
|  |         true, | ||
|  |         1002, | ||
|  |         'WS_ERR_INVALID_OPCODE' | ||
|  |       ); | ||
|  |     } | ||
|  | 
 | ||
|  |     if (!this._fin && !this._fragmented) this._fragmented = this._opcode; | ||
|  |     this._masked = (buf[1] & 0x80) === 0x80; | ||
|  | 
 | ||
|  |     if (this._isServer) { | ||
|  |       if (!this._masked) { | ||
|  |         this._loop = false; | ||
|  |         return error( | ||
|  |           RangeError, | ||
|  |           'MASK must be set', | ||
|  |           true, | ||
|  |           1002, | ||
|  |           'WS_ERR_EXPECTED_MASK' | ||
|  |         ); | ||
|  |       } | ||
|  |     } else if (this._masked) { | ||
|  |       this._loop = false; | ||
|  |       return error( | ||
|  |         RangeError, | ||
|  |         'MASK must be clear', | ||
|  |         true, | ||
|  |         1002, | ||
|  |         'WS_ERR_UNEXPECTED_MASK' | ||
|  |       ); | ||
|  |     } | ||
|  | 
 | ||
|  |     if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16; | ||
|  |     else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64; | ||
|  |     else return this.haveLength(); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Gets extended payload length (7+16). | ||
|  |    * | ||
|  |    * @return {(RangeError|undefined)} A possible error | ||
|  |    * @private | ||
|  |    */ | ||
|  |   getPayloadLength16() { | ||
|  |     if (this._bufferedBytes < 2) { | ||
|  |       this._loop = false; | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     this._payloadLength = this.consume(2).readUInt16BE(0); | ||
|  |     return this.haveLength(); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Gets extended payload length (7+64). | ||
|  |    * | ||
|  |    * @return {(RangeError|undefined)} A possible error | ||
|  |    * @private | ||
|  |    */ | ||
|  |   getPayloadLength64() { | ||
|  |     if (this._bufferedBytes < 8) { | ||
|  |       this._loop = false; | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     const buf = this.consume(8); | ||
|  |     const num = buf.readUInt32BE(0); | ||
|  | 
 | ||
|  |     //
 | ||
|  |     // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned
 | ||
|  |     // if payload length is greater than this number.
 | ||
|  |     //
 | ||
|  |     if (num > Math.pow(2, 53 - 32) - 1) { | ||
|  |       this._loop = false; | ||
|  |       return error( | ||
|  |         RangeError, | ||
|  |         'Unsupported WebSocket frame: payload length > 2^53 - 1', | ||
|  |         false, | ||
|  |         1009, | ||
|  |         'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH' | ||
|  |       ); | ||
|  |     } | ||
|  | 
 | ||
|  |     this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4); | ||
|  |     return this.haveLength(); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Payload length has been read. | ||
|  |    * | ||
|  |    * @return {(RangeError|undefined)} A possible error | ||
|  |    * @private | ||
|  |    */ | ||
|  |   haveLength() { | ||
|  |     if (this._payloadLength && this._opcode < 0x08) { | ||
|  |       this._totalPayloadLength += this._payloadLength; | ||
|  |       if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { | ||
|  |         this._loop = false; | ||
|  |         return error( | ||
|  |           RangeError, | ||
|  |           'Max payload size exceeded', | ||
|  |           false, | ||
|  |           1009, | ||
|  |           'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' | ||
|  |         ); | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     if (this._masked) this._state = GET_MASK; | ||
|  |     else this._state = GET_DATA; | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Reads mask bytes. | ||
|  |    * | ||
|  |    * @private | ||
|  |    */ | ||
|  |   getMask() { | ||
|  |     if (this._bufferedBytes < 4) { | ||
|  |       this._loop = false; | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     this._mask = this.consume(4); | ||
|  |     this._state = GET_DATA; | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Reads data bytes. | ||
|  |    * | ||
|  |    * @param {Function} cb Callback | ||
|  |    * @return {(Error|RangeError|undefined)} A possible error | ||
|  |    * @private | ||
|  |    */ | ||
|  |   getData(cb) { | ||
|  |     let data = EMPTY_BUFFER; | ||
|  | 
 | ||
|  |     if (this._payloadLength) { | ||
|  |       if (this._bufferedBytes < this._payloadLength) { | ||
|  |         this._loop = false; | ||
|  |         return; | ||
|  |       } | ||
|  | 
 | ||
|  |       data = this.consume(this._payloadLength); | ||
|  | 
 | ||
|  |       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); | ||
|  | 
 | ||
|  |     if (this._compressed) { | ||
|  |       this._state = INFLATING; | ||
|  |       this.decompress(data, cb); | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (data.length) { | ||
|  |       //
 | ||
|  |       // This message is not compressed so its length is the sum of the payload
 | ||
|  |       // length of all fragments.
 | ||
|  |       //
 | ||
|  |       this._messageLength = this._totalPayloadLength; | ||
|  |       this._fragments.push(data); | ||
|  |     } | ||
|  | 
 | ||
|  |     return this.dataMessage(); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Decompresses data. | ||
|  |    * | ||
|  |    * @param {Buffer} data Compressed data | ||
|  |    * @param {Function} cb Callback | ||
|  |    * @private | ||
|  |    */ | ||
|  |   decompress(data, cb) { | ||
|  |     const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; | ||
|  | 
 | ||
|  |     perMessageDeflate.decompress(data, this._fin, (err, buf) => { | ||
|  |       if (err) return cb(err); | ||
|  | 
 | ||
|  |       if (buf.length) { | ||
|  |         this._messageLength += buf.length; | ||
|  |         if (this._messageLength > this._maxPayload && this._maxPayload > 0) { | ||
|  |           return cb( | ||
|  |             error( | ||
|  |               RangeError, | ||
|  |               'Max payload size exceeded', | ||
|  |               false, | ||
|  |               1009, | ||
|  |               'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' | ||
|  |             ) | ||
|  |           ); | ||
|  |         } | ||
|  | 
 | ||
|  |         this._fragments.push(buf); | ||
|  |       } | ||
|  | 
 | ||
|  |       const er = this.dataMessage(); | ||
|  |       if (er) return cb(er); | ||
|  | 
 | ||
|  |       this.startLoop(cb); | ||
|  |     }); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Handles a data message. | ||
|  |    * | ||
|  |    * @return {(Error|undefined)} A possible error | ||
|  |    * @private | ||
|  |    */ | ||
|  |   dataMessage() { | ||
|  |     if (this._fin) { | ||
|  |       const messageLength = this._messageLength; | ||
|  |       const fragments = this._fragments; | ||
|  | 
 | ||
|  |       this._totalPayloadLength = 0; | ||
|  |       this._messageLength = 0; | ||
|  |       this._fragmented = 0; | ||
|  |       this._fragments = []; | ||
|  | 
 | ||
|  |       if (this._opcode === 2) { | ||
|  |         let data; | ||
|  | 
 | ||
|  |         if (this._binaryType === 'nodebuffer') { | ||
|  |           data = concat(fragments, messageLength); | ||
|  |         } else if (this._binaryType === 'arraybuffer') { | ||
|  |           data = toArrayBuffer(concat(fragments, messageLength)); | ||
|  |         } else { | ||
|  |           data = fragments; | ||
|  |         } | ||
|  | 
 | ||
|  |         this.emit('message', data, true); | ||
|  |       } else { | ||
|  |         const buf = concat(fragments, messageLength); | ||
|  | 
 | ||
|  |         if (!this._skipUTF8Validation && !isValidUTF8(buf)) { | ||
|  |           this._loop = false; | ||
|  |           return error( | ||
|  |             Error, | ||
|  |             'invalid UTF-8 sequence', | ||
|  |             true, | ||
|  |             1007, | ||
|  |             'WS_ERR_INVALID_UTF8' | ||
|  |           ); | ||
|  |         } | ||
|  | 
 | ||
|  |         this.emit('message', buf, false); | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     this._state = GET_INFO; | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Handles a control message. | ||
|  |    * | ||
|  |    * @param {Buffer} data Data to handle | ||
|  |    * @return {(Error|RangeError|undefined)} A possible error | ||
|  |    * @private | ||
|  |    */ | ||
|  |   controlMessage(data) { | ||
|  |     if (this._opcode === 0x08) { | ||
|  |       this._loop = false; | ||
|  | 
 | ||
|  |       if (data.length === 0) { | ||
|  |         this.emit('conclude', 1005, EMPTY_BUFFER); | ||
|  |         this.end(); | ||
|  |       } else if (data.length === 1) { | ||
|  |         return error( | ||
|  |           RangeError, | ||
|  |           'invalid payload length 1', | ||
|  |           true, | ||
|  |           1002, | ||
|  |           'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' | ||
|  |         ); | ||
|  |       } else { | ||
|  |         const code = data.readUInt16BE(0); | ||
|  | 
 | ||
|  |         if (!isValidStatusCode(code)) { | ||
|  |           return error( | ||
|  |             RangeError, | ||
|  |             `invalid status code ${code}`, | ||
|  |             true, | ||
|  |             1002, | ||
|  |             'WS_ERR_INVALID_CLOSE_CODE' | ||
|  |           ); | ||
|  |         } | ||
|  | 
 | ||
|  |         const buf = data.slice(2); | ||
|  | 
 | ||
|  |         if (!this._skipUTF8Validation && !isValidUTF8(buf)) { | ||
|  |           return error( | ||
|  |             Error, | ||
|  |             'invalid UTF-8 sequence', | ||
|  |             true, | ||
|  |             1007, | ||
|  |             'WS_ERR_INVALID_UTF8' | ||
|  |           ); | ||
|  |         } | ||
|  | 
 | ||
|  |         this.emit('conclude', code, buf); | ||
|  |         this.end(); | ||
|  |       } | ||
|  |     } else if (this._opcode === 0x09) { | ||
|  |       this.emit('ping', data); | ||
|  |     } else { | ||
|  |       this.emit('pong', data); | ||
|  |     } | ||
|  | 
 | ||
|  |     this._state = GET_INFO; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | module.exports = Receiver; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Builds an error object. | ||
|  |  * | ||
|  |  * @param {function(new:Error|RangeError)} ErrorCtor The error constructor | ||
|  |  * @param {String} message The error message | ||
|  |  * @param {Boolean} prefix Specifies whether or not to add a default prefix to | ||
|  |  *     `message` | ||
|  |  * @param {Number} statusCode The status code | ||
|  |  * @param {String} errorCode The exposed error code | ||
|  |  * @return {(Error|RangeError)} The error | ||
|  |  * @private | ||
|  |  */ | ||
|  | function error(ErrorCtor, message, prefix, statusCode, errorCode) { | ||
|  |   const err = new ErrorCtor( | ||
|  |     prefix ? `Invalid WebSocket frame: ${message}` : message | ||
|  |   ); | ||
|  | 
 | ||
|  |   Error.captureStackTrace(err, error); | ||
|  |   err.code = errorCode; | ||
|  |   err[kStatusCode] = statusCode; | ||
|  |   return err; | ||
|  | } |