| 'use strict'; |
| |
| var Buffer = require('safe-buffer').Buffer, |
| Emitter = require('events').EventEmitter, |
| util = require('util'), |
| streams = require('../streams'), |
| Headers = require('./headers'), |
| Reader = require('./stream_reader'); |
| |
| var Base = function(request, url, options) { |
| Emitter.call(this); |
| Base.validateOptions(options || {}, ['maxLength', 'masking', 'requireMasking', 'protocols']); |
| |
| this._request = request; |
| this._reader = new Reader(); |
| this._options = options || {}; |
| this._maxLength = this._options.maxLength || this.MAX_LENGTH; |
| this._headers = new Headers(); |
| this.__queue = []; |
| this.readyState = 0; |
| this.url = url; |
| |
| this.io = new streams.IO(this); |
| this.messages = new streams.Messages(this); |
| this._bindEventListeners(); |
| }; |
| util.inherits(Base, Emitter); |
| |
| Base.isWebSocket = function(request) { |
| var connection = request.headers.connection || '', |
| upgrade = request.headers.upgrade || ''; |
| |
| return request.method === 'GET' && |
| connection.toLowerCase().split(/ *, */).indexOf('upgrade') >= 0 && |
| upgrade.toLowerCase() === 'websocket'; |
| }; |
| |
| Base.validateOptions = function(options, validKeys) { |
| for (var key in options) { |
| if (validKeys.indexOf(key) < 0) |
| throw new Error('Unrecognized option: ' + key); |
| } |
| }; |
| |
| var instance = { |
| // This is 64MB, small enough for an average VPS to handle without |
| // crashing from process out of memory |
| MAX_LENGTH: 0x3ffffff, |
| |
| STATES: ['connecting', 'open', 'closing', 'closed'], |
| |
| _bindEventListeners: function() { |
| var self = this; |
| |
| // Protocol errors are informational and do not have to be handled |
| this.messages.on('error', function() {}); |
| |
| this.on('message', function(event) { |
| var messages = self.messages; |
| if (messages.readable) messages.emit('data', event.data); |
| }); |
| |
| this.on('error', function(error) { |
| var messages = self.messages; |
| if (messages.readable) messages.emit('error', error); |
| }); |
| |
| this.on('close', function() { |
| var messages = self.messages; |
| if (!messages.readable) return; |
| messages.readable = messages.writable = false; |
| messages.emit('end'); |
| }); |
| }, |
| |
| getState: function() { |
| return this.STATES[this.readyState] || null; |
| }, |
| |
| addExtension: function(extension) { |
| return false; |
| }, |
| |
| setHeader: function(name, value) { |
| if (this.readyState > 0) return false; |
| this._headers.set(name, value); |
| return true; |
| }, |
| |
| start: function() { |
| if (this.readyState !== 0) return false; |
| |
| if (!Base.isWebSocket(this._request)) |
| return this._failHandshake(new Error('Not a WebSocket request')); |
| |
| var response; |
| |
| try { |
| response = this._handshakeResponse(); |
| } catch (error) { |
| return this._failHandshake(error); |
| } |
| |
| this._write(response); |
| if (this._stage !== -1) this._open(); |
| return true; |
| }, |
| |
| _failHandshake: function(error) { |
| var headers = new Headers(); |
| headers.set('Content-Type', 'text/plain'); |
| headers.set('Content-Length', Buffer.byteLength(error.message, 'utf8')); |
| |
| headers = ['HTTP/1.1 400 Bad Request', headers.toString(), error.message]; |
| this._write(Buffer.from(headers.join('\r\n'), 'utf8')); |
| this._fail('protocol_error', error.message); |
| |
| return false; |
| }, |
| |
| text: function(message) { |
| return this.frame(message); |
| }, |
| |
| binary: function(message) { |
| return false; |
| }, |
| |
| ping: function() { |
| return false; |
| }, |
| |
| pong: function() { |
| return false; |
| }, |
| |
| close: function(reason, code) { |
| if (this.readyState !== 1) return false; |
| this.readyState = 3; |
| this.emit('close', new Base.CloseEvent(null, null)); |
| return true; |
| }, |
| |
| _open: function() { |
| this.readyState = 1; |
| this.__queue.forEach(function(args) { this.frame.apply(this, args) }, this); |
| this.__queue = []; |
| this.emit('open', new Base.OpenEvent()); |
| }, |
| |
| _queue: function(message) { |
| this.__queue.push(message); |
| return true; |
| }, |
| |
| _write: function(chunk) { |
| var io = this.io; |
| if (io.readable) io.emit('data', chunk); |
| }, |
| |
| _fail: function(type, message) { |
| this.readyState = 2; |
| this.emit('error', new Error(message)); |
| this.close(); |
| } |
| }; |
| |
| for (var key in instance) |
| Base.prototype[key] = instance[key]; |
| |
| |
| Base.ConnectEvent = function() {}; |
| |
| Base.OpenEvent = function() {}; |
| |
| Base.CloseEvent = function(code, reason) { |
| this.code = code; |
| this.reason = reason; |
| }; |
| |
| Base.MessageEvent = function(data) { |
| this.data = data; |
| }; |
| |
| Base.PingEvent = function(data) { |
| this.data = data; |
| }; |
| |
| Base.PongEvent = function(data) { |
| this.data = data; |
| }; |
| |
| module.exports = Base; |