You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
193 lines
4.5 KiB
193 lines
4.5 KiB
'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;
|
|
|