123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679 |
- /**
- * @file Represents a connection (both client and server sides)
- */
- 'use strict'
- var util = require('util'),
- events = require('events'),
- crypto = require('crypto'),
- InStream = require('./InStream'),
- OutStream = require('./OutStream'),
- frame = require('./frame'),
- Server = require('./Server')
- /**
- * @typedef {Object} Connection~Options
- * @param {string} path
- * @param {string} host
- * @param {?Object<string>} extraHeaders
- * @param {?Array<string>} protocols
- */
- /**
- * @class
- * @param {(net.Socket|tls.CleartextStream)} socket a net or tls socket
- * @param {(Server|Connection~Options)} parentOrOptions parent in case of server-side connection, object in case of client-side
- * @param {Function} [callback] will be added as a listener to 'connect'
- * @inherits EventEmitter
- * @event close the numeric code and string reason will be passed
- * @event error an error object is passed
- * @event text a string is passed
- * @event binary a inStream object is passed
- * @event pong a string is passed
- * @event connect
- */
- function Connection(socket, parentOrOptions, callback) {
- var that = this,
- connectEvent
- if (parentOrOptions instanceof Server) {
- // Server-side connection
- this.server = parentOrOptions
- this.path = null
- this.host = null
- this.extraHeaders = null
- this.protocols = []
- } else {
- // Client-side
- this.server = null
- this.path = parentOrOptions.path
- this.host = parentOrOptions.host
- this.extraHeaders = parentOrOptions.extraHeaders
- this.protocols = parentOrOptions.protocols || []
- }
- this.protocol = undefined
- this.socket = socket
- this.readyState = this.CONNECTING
- this.buffer = Buffer.alloc(0)
- this.frameBuffer = null // string for text frames and InStream for binary frames
- this.outStream = null // current allocated OutStream object for sending binary frames
- this.key = null // the Sec-WebSocket-Key header
- this.headers = {} // read only map of header names and values. Header names are lower-cased
- // Set listeners
- socket.on('readable', function () {
- that.doRead()
- })
- socket.on('error', function (err) {
- that.emit('error', err)
- })
- if (!this.server) {
- connectEvent = socket.constructor.name === 'CleartextStream' ? 'secureConnect' : 'connect'
- socket.on(connectEvent, function () {
- that.startHandshake()
- })
- }
- // Close listeners
- var onclose = function () {
- if (that.readyState === that.CONNECTING || that.readyState === that.OPEN) {
- that.emit('close', 1006, '')
- }
- that.readyState = this.CLOSED
- if (that.frameBuffer instanceof InStream) {
- that.frameBuffer.end()
- that.frameBuffer = null
- }
- if (that.outStream instanceof OutStream) {
- that.outStream.end()
- that.outStream = null
- }
- }
- socket.once('close', onclose)
- socket.once('finish', onclose)
- // super constructor
- events.EventEmitter.call(this)
- if (callback) {
- this.once('connect', callback)
- }
- }
- util.inherits(Connection, events.EventEmitter)
- module.exports = Connection
- /**
- * Minimum size of a pack of binary data to send in a single frame
- * @property {number} binaryFragmentation
- */
- Connection.binaryFragmentation = 512 * 1024 // .5 MiB
- /**
- * The maximum size the internal Buffer can grow
- * If at any time it stays bigger than this, the connection will be closed with code 1009
- * This is a security measure, to avoid memory attacks
- * @property {number} maxBufferLength
- */
- Connection.maxBufferLength = 2 * 1024 * 1024 // 2 MiB
- /**
- * Possible ready states for the connection
- * @constant {number} CONNECTING
- * @constant {number} OPEN
- * @constant {number} CLOSING
- * @constant {number} CLOSED
- */
- Connection.prototype.CONNECTING = 0
- Connection.prototype.OPEN = 1
- Connection.prototype.CLOSING = 2
- Connection.prototype.CLOSED = 3
- /**
- * Send a given string to the other side
- * @param {string} str
- * @param {Function} [callback] will be executed when the data is finally written out
- */
- Connection.prototype.sendText = function (str, callback) {
- if (this.readyState === this.OPEN) {
- if (!this.outStream) {
- return this.socket.write(frame.createTextFrame(str, !this.server), callback)
- }
- this.emit('error', new Error('You can\'t send a text frame until you finish sending binary frames'))
- } else {
- this.emit('error', new Error('You can\'t write to a non-open connection'))
- }
- }
- /**
- * Request for a OutStream to send binary data
- * @returns {OutStream}
- */
- Connection.prototype.beginBinary = function () {
- if (this.readyState === this.OPEN) {
- if (!this.outStream) {
- return (this.outStream = new OutStream(this, Connection.binaryFragmentation))
- }
- this.emit('error', new Error('You can\'t send more binary frames until you finish sending the previous binary frames'))
- } else {
- this.emit('error', new Error('You can\'t write to a non-open connection'))
- }
- }
- /**
- * Sends a binary buffer at once
- * @param {Buffer} data
- * @param {Function} [callback] will be executed when the data is finally written out
- */
- Connection.prototype.sendBinary = function (data, callback) {
- if (this.readyState === this.OPEN) {
- if (!this.outStream) {
- return this.socket.write(frame.createBinaryFrame(data, !this.server, true, true), callback)
- }
- this.emit('error', new Error('You can\'t send more binary frames until you finish sending the previous binary frames'))
- } else {
- this.emit('error', new Error('You can\'t write to a non-open connection'))
- }
- }
- /**
- * Sends a text or binary frame
- * @param {string|Buffer} data
- * @param {Function} [callback] will be executed when the data is finally written out
- */
- Connection.prototype.send = function (data, callback) {
- if (typeof data === 'string') {
- this.sendText(data, callback)
- } else if (Buffer.isBuffer(data)) {
- this.sendBinary(data, callback)
- } else {
- throw new TypeError('data should be either a string or a Buffer instance')
- }
- }
- /**
- * Sends a ping to the remote
- * @param {string} [data=''] - optional ping data
- * @fires pong when pong reply is received
- */
- Connection.prototype.sendPing = function (data) {
- if (this.readyState === this.OPEN) {
- this.socket.write(frame.createPingFrame(data || '', !this.server))
- } else {
- this.emit('error', new Error('You can\'t write to a non-open connection'))
- }
- }
- /**
- * Close the connection, sending a close frame and waiting for response
- * If the connection isn't OPEN, closes it without sending a close frame
- * @param {number} [code]
- * @param {string} [reason]
- * @fires close
- */
- Connection.prototype.close = function (code, reason) {
- if (this.readyState === this.OPEN) {
- this.socket.write(frame.createCloseFrame(code, reason, !this.server))
- this.readyState = this.CLOSING
- } else if (this.readyState !== this.CLOSED) {
- this.socket.end()
- this.readyState = this.CLOSED
- }
- this.emit('close', code, reason)
- }
- /**
- * Reads contents from the socket and process it
- * @fires connect
- * @private
- */
- Connection.prototype.doRead = function () {
- var buffer, temp
- // Fetches the data
- buffer = this.socket.read()
- if (!buffer) {
- // Waits for more data
- return
- }
- // Save to the internal buffer
- this.buffer = Buffer.concat([this.buffer, buffer], this.buffer.length + buffer.length)
- if (this.readyState === this.CONNECTING) {
- if (!this.readHandshake()) {
- // May have failed or we're waiting for more data
- return
- }
- }
- if (this.readyState !== this.CLOSED) {
- // Try to read as many frames as possible
- while ((temp = this.extractFrame()) === true) {}
- if (temp === false) {
- // Protocol error
- this.close(1002)
- } else if (this.buffer.length > Connection.maxBufferLength) {
- // Frame too big
- this.close(1009)
- }
- }
- }
- /**
- * Create and send a handshake as a client
- * @private
- */
- Connection.prototype.startHandshake = function () {
- var str, i, key, headers, header
- key = Buffer.alloc(16)
- for (i = 0; i < 16; i++) {
- key[i] = Math.floor(Math.random() * 256)
- }
- this.key = key.toString('base64')
- headers = {
- Host: this.host,
- Upgrade: 'websocket',
- Connection: 'Upgrade',
- 'Sec-WebSocket-Key': this.key,
- 'Sec-WebSocket-Version': '13'
- }
- if (this.protocols && this.protocols.length) {
- headers['Sec-WebSocket-Protocol'] = this.protocols.join(', ')
- }
- for (header in this.extraHeaders) {
- headers[header] = this.extraHeaders[header]
- }
- str = this.buildRequest('GET ' + this.path + ' HTTP/1.1', headers)
- this.socket.write(str)
- }
- /**
- * Try to read the handshake from the internal buffer
- * If it succeeds, the handshake data is consumed from the internal buffer
- * @returns {boolean} - whether the handshake was done
- * @private
- */
- Connection.prototype.readHandshake = function () {
- var found = false,
- i, data
- // Do the handshake and try to connect
- if (this.buffer.length > Connection.maxBufferLength) {
- // Too big for a handshake
- if (this.server) {
- this.socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
- } else {
- this.socket.end()
- this.emit('error', new Error('Handshake is too big'))
- }
- return false
- }
- // Search for '\r\n\r\n'
- for (i = 0; i < this.buffer.length - 3; i++) {
- if (this.buffer[i] === 13 && this.buffer[i + 2] === 13 &&
- this.buffer[i + 1] === 10 && this.buffer[i + 3] === 10) {
- found = true
- break
- }
- }
- if (!found) {
- // Wait for more data
- return false
- }
- data = this.buffer.slice(0, i + 4).toString().split('\r\n')
- if (this.server ? this.answerHandshake(data) : this.checkHandshake(data)) {
- this.buffer = this.buffer.slice(i + 4)
- this.readyState = this.OPEN
- this.emit('connect')
- return true
- } else {
- this.socket.end(this.server ? 'HTTP/1.1 400 Bad Request\r\n\r\n' : undefined)
- return false
- }
- }
- /**
- * Read headers from HTTP protocol
- * Update the Connection#headers property
- * @param {string[]} lines one for each '\r\n'-separated HTTP request line
- * @private
- */
- Connection.prototype.readHeaders = function (lines) {
- var i, match
- // Extract all headers
- // Ignore bad-formed lines and ignore the first line (HTTP header)
- for (i = 1; i < lines.length; i++) {
- if ((match = lines[i].match(/^([a-z-]+): (.+)$/i))) {
- this.headers[match[1].toLowerCase()] = match[2]
- }
- }
- }
- /**
- * Process and check a handshake answered by a server
- * @param {string[]} lines one for each '\r\n'-separated HTTP request line
- * @returns {boolean} if the handshake was sucessful. If not, the connection must be closed
- * @private
- */
- Connection.prototype.checkHandshake = function (lines) {
- var key, sha1, protocol
- // First line
- if (lines.length < 4) {
- this.emit('error', new Error('Invalid handshake: too short'))
- return false
- }
- if (!lines[0].match(/^HTTP\/\d\.\d 101( .*)?$/i)) {
- this.emit('error', new Error('Invalid handshake: invalid first line format'))
- return false
- }
- // Extract all headers
- this.readHeaders(lines)
- // Validate necessary headers
- if (!('upgrade' in this.headers) ||
- !('sec-websocket-accept' in this.headers) ||
- !('connection' in this.headers)) {
- this.emit('error', new Error('Invalid handshake: missing required headers'))
- return false
- }
- if (this.headers.upgrade.toLowerCase() !== 'websocket' ||
- this.headers.connection.toLowerCase().split(/\s*,\s*/).indexOf('upgrade') === -1) {
- this.emit('error', new Error('Invalid handshake: invalid Upgrade or Connection header'))
- return false
- }
- key = this.headers['sec-websocket-accept']
- // Check protocol negotiation
- protocol = this.headers['sec-websocket-protocol']
- if (this.protocols && this.protocols.length) {
- // The server must choose one from our list
- if (!protocol || this.protocols.indexOf(protocol) === -1) {
- this.emit('error', new Error('Invalid handshake: no protocol was negotiated'))
- return false
- }
- } else {
- // The server must not choose a protocol
- if (protocol) {
- this.emit('error', new Error('Invalid handshake: no protocol negotiation was expected'))
- return false
- }
- }
- this.protocol = protocol
- // Check the key
- sha1 = crypto.createHash('sha1')
- sha1.end(this.key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
- if (key !== sha1.read().toString('base64')) {
- this.emit('error', new Error('Invalid handshake: hash mismatch'))
- return false
- }
- return true
- }
- /**
- * Process and answer a handshake started by a client
- * @param {string[]} lines one for each '\r\n'-separated HTTP request line
- * @returns {boolean} if the handshake was sucessful. If not, the connection must be closed with error 400-Bad Request
- * @private
- */
- Connection.prototype.answerHandshake = function (lines) {
- var path, key, sha1, headers
- // First line
- if (lines.length < 6) {
- return false
- }
- path = lines[0].match(/^GET (.+) HTTP\/\d\.\d$/i)
- if (!path) {
- return false
- }
- this.path = path[1]
- // Extract all headers
- this.readHeaders(lines)
- // Validate necessary headers
- if (!('host' in this.headers) ||
- !('sec-websocket-key' in this.headers) ||
- !('upgrade' in this.headers) ||
- !('connection' in this.headers)) {
- return false
- }
- if (this.headers.upgrade.toLowerCase() !== 'websocket' ||
- this.headers.connection.toLowerCase().split(/\s*,\s*/).indexOf('upgrade') === -1) {
- return false
- }
- if (this.headers['sec-websocket-version'] !== '13') {
- return false
- }
- this.key = this.headers['sec-websocket-key']
- // Agree on a protocol
- if ('sec-websocket-protocol' in this.headers) {
- // Parse
- this.protocols = this.headers['sec-websocket-protocol'].split(',').map(function (each) {
- return each.trim()
- })
- // Select protocol
- if (this.server._selectProtocol) {
- this.protocol = this.server._selectProtocol(this, this.protocols)
- }
- }
- // Build and send the response
- sha1 = crypto.createHash('sha1')
- sha1.end(this.key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
- key = sha1.read().toString('base64')
- headers = {
- Upgrade: 'websocket',
- Connection: 'Upgrade',
- 'Sec-WebSocket-Accept': key
- }
- if (this.protocol) {
- headers['Sec-WebSocket-Protocol'] = this.protocol
- }
- this.socket.write(this.buildRequest('HTTP/1.1 101 Switching Protocols', headers))
- return true
- }
- /**
- * Try to extract frame contents from the buffer (and execute it)
- * @returns {(boolean|undefined)} false=something went wrong (the connection must be closed); undefined=there isn't enough data to catch a frame; true=the frame was successfully fetched and executed
- * @private
- */
- Connection.prototype.extractFrame = function () {
- var fin, opcode, B, HB, mask, len, payload, start, i, hasMask
- if (this.buffer.length < 2) {
- return
- }
- // Is this the last frame in a sequence?
- B = this.buffer[0]
- HB = B >> 4
- if (HB % 8) {
- // RSV1, RSV2 and RSV3 must be clear
- return false
- }
- fin = HB === 8
- opcode = B % 16
- if (opcode !== 0 && opcode !== 1 && opcode !== 2 &&
- opcode !== 8 && opcode !== 9 && opcode !== 10) {
- // Invalid opcode
- return false
- }
- if (opcode >= 8 && !fin) {
- // Control frames must not be fragmented
- return false
- }
- B = this.buffer[1]
- hasMask = B >> 7
- if ((this.server && !hasMask) || (!this.server && hasMask)) {
- // Frames sent by clients must be masked
- return false
- }
- len = B % 128
- start = hasMask ? 6 : 2
- if (this.buffer.length < start + len) {
- // Not enough data in the buffer
- return
- }
- // Get the actual payload length
- if (len === 126) {
- len = this.buffer.readUInt16BE(2)
- start += 2
- } else if (len === 127) {
- // Warning: JS can only store up to 2^53 in its number format
- len = this.buffer.readUInt32BE(2) * Math.pow(2, 32) + this.buffer.readUInt32BE(6)
- start += 8
- }
- if (this.buffer.length < start + len) {
- return
- }
- // Extract the payload
- payload = this.buffer.slice(start, start + len)
- if (hasMask) {
- // Decode with the given mask
- mask = this.buffer.slice(start - 4, start)
- for (i = 0; i < payload.length; i++) {
- payload[i] ^= mask[i % 4]
- }
- }
- this.buffer = this.buffer.slice(start + len)
- // Proceeds to frame processing
- return this.processFrame(fin, opcode, payload)
- }
- /**
- * Process a given frame received
- * @param {boolean} fin
- * @param {number} opcode
- * @param {Buffer} payload
- * @returns {boolean} false if any error occurs, true otherwise
- * @fires text
- * @fires binary
- * @private
- */
- Connection.prototype.processFrame = function (fin, opcode, payload) {
- if (opcode === 8) {
- // Close frame
- if (this.readyState === this.CLOSING) {
- this.socket.end()
- } else if (this.readyState === this.OPEN) {
- this.processCloseFrame(payload)
- }
- return true
- } else if (opcode === 9) {
- // Ping frame
- if (this.readyState === this.OPEN) {
- this.socket.write(frame.createPongFrame(payload.toString(), !this.server))
- }
- return true
- } else if (opcode === 10) {
- // Pong frame
- this.emit('pong', payload.toString())
- return true
- }
- if (this.readyState !== this.OPEN) {
- // Ignores if the connection isn't opened anymore
- return true
- }
- if (opcode === 0 && this.frameBuffer === null) {
- // Unexpected continuation frame
- return false
- } else if (opcode !== 0 && this.frameBuffer !== null) {
- // Last sequence didn't finished correctly
- return false
- }
- if (!opcode) {
- // Get the current opcode for fragmented frames
- opcode = typeof this.frameBuffer === 'string' ? 1 : 2
- }
- if (opcode === 1) {
- // Save text frame
- payload = payload.toString()
- this.frameBuffer = this.frameBuffer ? this.frameBuffer + payload : payload
- if (fin) {
- // Emits 'text' event
- this.emit('text', this.frameBuffer)
- this.frameBuffer = null
- }
- } else {
- // Sends the buffer for InStream object
- if (!this.frameBuffer) {
- // Emits the 'binary' event
- this.frameBuffer = new InStream
- this.emit('binary', this.frameBuffer)
- }
- this.frameBuffer.addData(payload)
- if (fin) {
- // Emits 'end' event
- this.frameBuffer.end()
- this.frameBuffer = null
- }
- }
- return true
- }
- /**
- * Process a close frame, emitting the close event and sending back the frame
- * @param {Buffer} payload
- * @fires close
- * @private
- */
- Connection.prototype.processCloseFrame = function (payload) {
- var code, reason
- if (payload.length >= 2) {
- code = payload.readUInt16BE(0)
- reason = payload.slice(2).toString()
- } else {
- code = 1005
- reason = ''
- }
- this.socket.write(frame.createCloseFrame(code, reason, !this.server))
- this.readyState = this.CLOSED
- this.emit('close', code, reason)
- }
- /**
- * Build the header string
- * @param {string} requestLine
- * @param {Object<string>} headers
- * @returns {string}
- * @private
- */
- Connection.prototype.buildRequest = function (requestLine, headers) {
- var headerString = requestLine + '\r\n',
- headerName
- for (headerName in headers) {
- headerString += headerName + ': ' + headers[headerName] + '\r\n'
- }
- return headerString + '\r\n'
- }
|