fix
This commit is contained in:
parent
70c7b38f16
commit
1e50271fac
|
@ -0,0 +1,374 @@
|
|||
(function(){
|
||||
var WebSocketServer=require('ws').Server;
|
||||
var wss=new WebSocketServer({port:8080});
|
||||
var bannedKeys=[];
|
||||
var bannedIps=[];
|
||||
|
||||
var rooms=[{},{},{},{},{},{},{},{}];
|
||||
var events=[];
|
||||
var clients={};
|
||||
var bannedKeyWords=[];
|
||||
var messages={
|
||||
enter:function(index,nickname,avatar,config,mode){
|
||||
this.nickname=nickname;
|
||||
this.avatar=avatar;
|
||||
var room=rooms[index];
|
||||
if(!room){
|
||||
index=0;
|
||||
room=rooms[0];
|
||||
}
|
||||
this.room=room;
|
||||
delete this.status;
|
||||
if(room.owner){
|
||||
if(room.servermode&&!room.owner._onconfig&&config&&mode){
|
||||
room.owner.sendl('createroom',index,config,mode);
|
||||
room.owner._onconfig=this;
|
||||
room.owner.nickname=nickname;
|
||||
room.owner.avatar=avatar;
|
||||
}
|
||||
else if(!room.config){
|
||||
this.sendl('enterroomfailed');
|
||||
}
|
||||
else{
|
||||
this.owner=room.owner;
|
||||
this.owner.sendl('onconnection',this.wsid);
|
||||
}
|
||||
util.updaterooms();
|
||||
}
|
||||
else{
|
||||
room.owner=this;
|
||||
this.sendl('createroom',index);
|
||||
}
|
||||
},
|
||||
changeAvatar:function(nickname,avatar){
|
||||
this.nickname=nickname;
|
||||
this.avatar=avatar;
|
||||
util.updateclients();
|
||||
},
|
||||
server:function(cfg){
|
||||
if(cfg){
|
||||
this.servermode=true;
|
||||
var room=rooms[cfg[0]];
|
||||
if(!room||room.owner){
|
||||
this.sendl('reloadroom',true);
|
||||
}
|
||||
else{
|
||||
room.owner=this;
|
||||
this.room=room;
|
||||
this.nickname=cfg[1];
|
||||
this.avatar=cfg[2];
|
||||
this.sendl('createroom',cfg[0],{},'auto')
|
||||
}
|
||||
}
|
||||
else{
|
||||
for(var i=0;i<rooms.length;i++){
|
||||
if(!rooms[i].owner){
|
||||
rooms[i].owner=this;
|
||||
rooms[i].servermode=true;
|
||||
this.room=rooms[i];
|
||||
this.servermode=true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
util.updaterooms();
|
||||
}
|
||||
},
|
||||
key:function(id){
|
||||
if(!id||typeof id!='object'){
|
||||
this.sendl('denied','key');
|
||||
this.close();
|
||||
clearTimeout(this.keyCheck);
|
||||
delete this.keyCheck;
|
||||
return;
|
||||
}
|
||||
else if(bannedKeys.indexOf(id[0])!=-1){
|
||||
bannedIps.push(this._socket.remoteAddress);
|
||||
this.close();
|
||||
}
|
||||
this.onlineKey=id[0];
|
||||
clearTimeout(this.keyCheck);
|
||||
delete this.keyCheck;
|
||||
},
|
||||
events:function(cfg,id,type){
|
||||
if(bannedKeys.indexOf(id)!=-1||typeof id!='string'){
|
||||
bannedIps.push(this._socket.remoteAddress);
|
||||
console.log(id, this._socket.remoteAddress);
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
var changed=false;
|
||||
var time=(new Date()).getTime();
|
||||
if(cfg&&id){
|
||||
if(typeof cfg=='string'){
|
||||
for(var i=0;i<events.length;i++){
|
||||
if(events[i].id==cfg){
|
||||
if(type=='join'){
|
||||
if(events[i].members.indexOf(id)==-1){
|
||||
events[i].members.push(id);
|
||||
}
|
||||
changed=true;
|
||||
}
|
||||
else if(type=='leave'){
|
||||
var index=events[i].members.indexOf(id);
|
||||
if(index!=-1){
|
||||
events[i].members.splice(index,1);
|
||||
if(events[i].members.length==0){
|
||||
events.splice(i--,1);
|
||||
}
|
||||
}
|
||||
changed=true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(cfg.hasOwnProperty('utc')&&
|
||||
cfg.hasOwnProperty('day')&&
|
||||
cfg.hasOwnProperty('hour')&&
|
||||
cfg.hasOwnProperty('content')){
|
||||
if(events.length>=20){
|
||||
this.sendl('eventsdenied','total');
|
||||
}
|
||||
else if(cfg.utc<=time){
|
||||
this.sendl('eventsdenied','time');
|
||||
}
|
||||
else if(util.isBanned(cfg.content)){
|
||||
this.sendl('eventsdenied','ban');
|
||||
}
|
||||
else{
|
||||
cfg.nickname=cfg.nickname||'无名玩家';
|
||||
cfg.avatar=cfg.nickname||'caocao';
|
||||
cfg.creator=id;
|
||||
cfg.id=util.getid();
|
||||
cfg.members=[id];
|
||||
events.unshift(cfg);
|
||||
changed=true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(changed){
|
||||
util.updateevents();
|
||||
}
|
||||
},
|
||||
config:function(config){
|
||||
var room=this.room;
|
||||
if(room&&room.owner==this){
|
||||
if(room.servermode){
|
||||
room.servermode=false;
|
||||
if(this._onconfig){
|
||||
if(clients[this._onconfig.wsid]){
|
||||
this._onconfig.owner=this;
|
||||
this.sendl('onconnection',this._onconfig.wsid);
|
||||
}
|
||||
delete this._onconfig;
|
||||
}
|
||||
}
|
||||
room.config=config;
|
||||
}
|
||||
util.updaterooms();
|
||||
},
|
||||
status:function(str){
|
||||
if(typeof str=='string'){
|
||||
this.status=str;
|
||||
}
|
||||
else{
|
||||
delete this.status;
|
||||
}
|
||||
util.updateclients();
|
||||
},
|
||||
send:function(id,message){
|
||||
if(clients[id]&&clients[id].owner==this){
|
||||
try{
|
||||
clients[id].send(message);
|
||||
}
|
||||
catch(e){
|
||||
clients[id].close();
|
||||
}
|
||||
}
|
||||
},
|
||||
close:function(id){
|
||||
if(clients[id]&&clients[id].owner==this){
|
||||
clients[id].close();
|
||||
}
|
||||
},
|
||||
};
|
||||
var util={
|
||||
isBanned:function(str){
|
||||
for(var i of bannedKeyWords){
|
||||
if(str.indexOf(i)!=-1) return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
sendl:function(){
|
||||
var args=[];
|
||||
for(var i=0;i<arguments.length;i++){
|
||||
args.push(arguments[i]);
|
||||
}
|
||||
try{
|
||||
this.send(JSON.stringify(args));
|
||||
}
|
||||
catch(e){
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
getid:function(){
|
||||
return (Math.floor(1000000000+9000000000*Math.random())).toString();
|
||||
},
|
||||
getroomlist:function(){
|
||||
var roomlist=[];
|
||||
for(var i=0;i<rooms.length;i++){
|
||||
rooms[i]._num=0;
|
||||
}
|
||||
for(var i in clients){
|
||||
if(clients[i].room&&!clients[i].servermode){
|
||||
clients[i].room._num++;
|
||||
}
|
||||
}
|
||||
for(var i=0;i<rooms.length;i++){
|
||||
if(rooms[i].servermode){
|
||||
roomlist[i]='server';
|
||||
}
|
||||
else if(rooms[i].owner&&rooms[i].config){
|
||||
if(rooms[i]._num==0){
|
||||
rooms[i].owner.sendl('reloadroom');
|
||||
}
|
||||
roomlist[i]=[rooms[i].owner.nickname,rooms[i].owner.avatar,
|
||||
rooms[i].config,rooms[i]._num];
|
||||
}
|
||||
else{
|
||||
roomlist[i]=null;
|
||||
}
|
||||
delete rooms[i]._num;
|
||||
}
|
||||
return roomlist;
|
||||
},
|
||||
getclientlist:function(){
|
||||
var clientlist=[];
|
||||
for(var i in clients){
|
||||
clientlist.push([clients[i].nickname,clients[i].avatar,!clients[i].room,clients[i].status,clients[i].wsid,clients[i].onlineKey]);
|
||||
}
|
||||
return clientlist;
|
||||
},
|
||||
updaterooms:function(){
|
||||
var roomlist=util.getroomlist();
|
||||
var clientlist=util.getclientlist();
|
||||
for(var i in clients){
|
||||
if(!clients[i].room){
|
||||
clients[i].sendl('updaterooms',roomlist,clientlist);
|
||||
}
|
||||
}
|
||||
},
|
||||
updateclients:function(){
|
||||
var clientlist=util.getclientlist();
|
||||
for(var i in clients){
|
||||
if(!clients[i].room){
|
||||
clients[i].sendl('updateclients',clientlist);
|
||||
}
|
||||
}
|
||||
},
|
||||
checkevents:function(){
|
||||
if(events.length){
|
||||
var time=(new Date()).getTime();
|
||||
for(var i=0;i<events.length;i++){
|
||||
if(events[i].utc<=time){
|
||||
events.splice(i--,1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return events;
|
||||
},
|
||||
updateevents:function(){
|
||||
util.checkevents();
|
||||
for(var i in clients){
|
||||
if(!clients[i].room){
|
||||
clients[i].sendl('updateevents',events);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
wss.on('connection',function(ws){
|
||||
ws.sendl=util.sendl;
|
||||
if(bannedIps.indexOf(ws._socket.remoteAddress)!=-1){
|
||||
ws.sendl('denied','banned');
|
||||
setTimeout(function(){
|
||||
ws.close();
|
||||
},500);
|
||||
return;
|
||||
}
|
||||
ws.keyCheck=setTimeout(function(){
|
||||
ws.sendl('denied','key');
|
||||
setTimeout(function(){
|
||||
ws.close();
|
||||
},500);
|
||||
},2000);
|
||||
ws.wsid=util.getid();
|
||||
clients[ws.wsid]=ws;
|
||||
ws.sendl('roomlist',util.getroomlist(),util.checkevents(),util.getclientlist(ws),ws.wsid);
|
||||
ws.heartbeat=setInterval(function(){
|
||||
if(ws.beat){
|
||||
ws.close();
|
||||
clearInterval(ws.heartbeat);
|
||||
}
|
||||
else{
|
||||
ws.beat=true;
|
||||
try{
|
||||
ws.send('heartbeat');
|
||||
}
|
||||
catch(e){
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
},60000);
|
||||
ws.on('message',function(message){
|
||||
if(!clients[this.wsid]) return;
|
||||
if(message=='heartbeat'){
|
||||
this.beat=false;
|
||||
}
|
||||
else if(this.owner){
|
||||
this.owner.sendl('onmessage',this.wsid,message);
|
||||
}
|
||||
else{
|
||||
var arr;
|
||||
try{
|
||||
arr=JSON.parse(message);
|
||||
if(!Array.isArray(arr)){
|
||||
throw('err');
|
||||
}
|
||||
}
|
||||
catch(e){
|
||||
this.sendl('denied','banned');
|
||||
return;
|
||||
}
|
||||
if(arr.shift()=='server'){
|
||||
var type=arr.shift();
|
||||
if(messages[type]){
|
||||
messages[type].apply(this,arr);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
ws.on('close',function(){
|
||||
for(var i=0;i<rooms.length;i++){
|
||||
if(rooms[i].owner==this){
|
||||
rooms[i].owner=null;
|
||||
rooms[i].config=null;
|
||||
rooms[i].servermode=false;
|
||||
for(var j in clients){
|
||||
if(clients[j].room==rooms[i]&&clients[j]!=this){
|
||||
clients[j].sendl('selfclose');
|
||||
// clients[j].close();
|
||||
// delete clients[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(clients[this.wsid]){
|
||||
if(this.owner){
|
||||
this.owner.sendl('onclose',this.wsid);
|
||||
}
|
||||
delete clients[this.wsid];
|
||||
}
|
||||
if(this.room) util.updaterooms();
|
||||
else util.updateclients();
|
||||
});
|
||||
});
|
||||
}());
|
|
@ -160,4 +160,25 @@ window.noname_source_list=[
|
|||
'theme/style/hp/image/xinround2.png',
|
||||
'theme/style/hp/image/xinround3.png',
|
||||
'theme/style/hp/image/xinround4.png',
|
||||
'node_modules/ws/index.js',
|
||||
'node_modules/ws/package.json',
|
||||
'node_modules/ws/lib/BufferPool.js',
|
||||
'node_modules/ws/lib/BufferUtil.fallback.js',
|
||||
'node_modules/ws/lib/BufferUtil.js',
|
||||
'node_modules/ws/lib/ErrorCodes.js',
|
||||
'node_modules/ws/lib/Extensions.js',
|
||||
'node_modules/ws/lib/PerMessageDeflate.js',
|
||||
'node_modules/ws/lib/Receiver.hixie.js',
|
||||
'node_modules/ws/lib/Receiver.js',
|
||||
'node_modules/ws/lib/Sender.hixie.js',
|
||||
'node_modules/ws/lib/Sender.js',
|
||||
'node_modules/ws/lib/Validation.fallback.js',
|
||||
'node_modules/ws/lib/Validation.js',
|
||||
'node_modules/ws/lib/WebSocket.js',
|
||||
'node_modules/ws/lib/WebSocketServer.js',
|
||||
'node_modules/ultron/index.js',
|
||||
'node_modules/ultron/package.json',
|
||||
'node_modules/ultron/test.js',
|
||||
'node_modules/options/package.json',
|
||||
'node_modules/options/lib/options.js',
|
||||
];
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
npm-debug.log
|
||||
node_modules
|
||||
.*.swp
|
||||
.lock-*
|
||||
build/
|
||||
|
||||
test
|
|
@ -0,0 +1,86 @@
|
|||
/*!
|
||||
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
var fs = require('fs');
|
||||
|
||||
function Options(defaults) {
|
||||
var internalValues = {};
|
||||
var values = this.value = {};
|
||||
Object.keys(defaults).forEach(function(key) {
|
||||
internalValues[key] = defaults[key];
|
||||
Object.defineProperty(values, key, {
|
||||
get: function() { return internalValues[key]; },
|
||||
configurable: false,
|
||||
enumerable: true
|
||||
});
|
||||
});
|
||||
this.reset = function() {
|
||||
Object.keys(defaults).forEach(function(key) {
|
||||
internalValues[key] = defaults[key];
|
||||
});
|
||||
return this;
|
||||
};
|
||||
this.merge = function(options, required) {
|
||||
options = options || {};
|
||||
if (Object.prototype.toString.call(required) === '[object Array]') {
|
||||
var missing = [];
|
||||
for (var i = 0, l = required.length; i < l; ++i) {
|
||||
var key = required[i];
|
||||
if (!(key in options)) {
|
||||
missing.push(key);
|
||||
}
|
||||
}
|
||||
if (missing.length > 0) {
|
||||
if (missing.length > 1) {
|
||||
throw new Error('options ' +
|
||||
missing.slice(0, missing.length - 1).join(', ') + ' and ' +
|
||||
missing[missing.length - 1] + ' must be defined');
|
||||
}
|
||||
else throw new Error('option ' + missing[0] + ' must be defined');
|
||||
}
|
||||
}
|
||||
Object.keys(options).forEach(function(key) {
|
||||
if (key in internalValues) {
|
||||
internalValues[key] = options[key];
|
||||
}
|
||||
});
|
||||
return this;
|
||||
};
|
||||
this.copy = function(keys) {
|
||||
var obj = {};
|
||||
Object.keys(defaults).forEach(function(key) {
|
||||
if (keys.indexOf(key) !== -1) {
|
||||
obj[key] = values[key];
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
};
|
||||
this.read = function(filename, cb) {
|
||||
if (typeof cb == 'function') {
|
||||
var self = this;
|
||||
fs.readFile(filename, function(error, data) {
|
||||
if (error) return cb(error);
|
||||
var conf = JSON.parse(data);
|
||||
self.merge(conf);
|
||||
cb();
|
||||
});
|
||||
}
|
||||
else {
|
||||
var conf = JSON.parse(fs.readFileSync(filename));
|
||||
this.merge(conf);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
this.isDefined = function(key) {
|
||||
return typeof values[key] != 'undefined';
|
||||
};
|
||||
this.isDefinedAndNonNull = function(key) {
|
||||
return typeof values[key] != 'undefined' && values[key] !== null;
|
||||
};
|
||||
Object.freeze(values);
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
module.exports = Options;
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"author": {
|
||||
"name": "Einar Otto Stangvik",
|
||||
"email": "einaros@gmail.com",
|
||||
"url": "http://2x.io"
|
||||
},
|
||||
"name": "options",
|
||||
"description": "A very light-weight in-code option parsers for node.js.",
|
||||
"version": "0.0.6",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/einaros/options.js.git"
|
||||
},
|
||||
"main": "lib/options",
|
||||
"scripts": {
|
||||
"test": "make test"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"mocha": "latest"
|
||||
},
|
||||
"gitHead": "ff53d0a092c897cb95964232a96fe17da65c11af",
|
||||
"bugs": {
|
||||
"url": "https://github.com/einaros/options.js/issues"
|
||||
},
|
||||
"homepage": "https://github.com/einaros/options.js",
|
||||
"_id": "options@0.0.6",
|
||||
"_shasum": "ec22d312806bb53e731773e7cdaefcf1c643128f",
|
||||
"_from": "options@>=0.0.5",
|
||||
"_npmVersion": "1.4.21",
|
||||
"_npmUser": {
|
||||
"name": "einaros",
|
||||
"email": "einaros@gmail.com"
|
||||
},
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "einaros",
|
||||
"email": "einaros@gmail.com"
|
||||
}
|
||||
],
|
||||
"dist": {
|
||||
"shasum": "ec22d312806bb53e731773e7cdaefcf1c643128f",
|
||||
"tarball": "http://registry.npmjs.org/options/-/options-0.0.6.tgz"
|
||||
},
|
||||
"directories": {},
|
||||
"_resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz",
|
||||
"readme": "ERROR: No README data found!"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
coverage
|
||||
.tern-port
|
|
@ -0,0 +1,21 @@
|
|||
sudo: false
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.12"
|
||||
- "0.10"
|
||||
- "0.8"
|
||||
- "iojs"
|
||||
before_install:
|
||||
- 'if [ "${TRAVIS_NODE_VERSION}" == "0.8" ]; then npm install -g npm@2.11.1; fi'
|
||||
script:
|
||||
- "npm run test-travis"
|
||||
after_script:
|
||||
- "npm install coveralls@2.11.x && cat coverage/lcov.info | coveralls"
|
||||
matrix:
|
||||
fast_finish: true
|
||||
notifications:
|
||||
irc:
|
||||
channels:
|
||||
- "irc.freenode.org#unshift"
|
||||
on_success: change
|
||||
on_failure: change
|
|
@ -0,0 +1,129 @@
|
|||
'use strict';
|
||||
|
||||
var has = Object.prototype.hasOwnProperty;
|
||||
|
||||
/**
|
||||
* An auto incrementing id which we can use to create "unique" Ultron instances
|
||||
* so we can track the event emitters that are added through the Ultron
|
||||
* interface.
|
||||
*
|
||||
* @type {Number}
|
||||
* @private
|
||||
*/
|
||||
var id = 0;
|
||||
|
||||
/**
|
||||
* Ultron is high-intelligence robot. It gathers intelligence so it can start improving
|
||||
* upon his rudimentary design. It will learn from your EventEmitting patterns
|
||||
* and exterminate them.
|
||||
*
|
||||
* @constructor
|
||||
* @param {EventEmitter} ee EventEmitter instance we need to wrap.
|
||||
* @api public
|
||||
*/
|
||||
function Ultron(ee) {
|
||||
if (!(this instanceof Ultron)) return new Ultron(ee);
|
||||
|
||||
this.id = id++;
|
||||
this.ee = ee;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new EventListener for the given event.
|
||||
*
|
||||
* @param {String} event Name of the event.
|
||||
* @param {Functon} fn Callback function.
|
||||
* @param {Mixed} context The context of the function.
|
||||
* @returns {Ultron}
|
||||
* @api public
|
||||
*/
|
||||
Ultron.prototype.on = function on(event, fn, context) {
|
||||
fn.__ultron = this.id;
|
||||
this.ee.on(event, fn, context);
|
||||
|
||||
return this;
|
||||
};
|
||||
/**
|
||||
* Add an EventListener that's only called once.
|
||||
*
|
||||
* @param {String} event Name of the event.
|
||||
* @param {Function} fn Callback function.
|
||||
* @param {Mixed} context The context of the function.
|
||||
* @returns {Ultron}
|
||||
* @api public
|
||||
*/
|
||||
Ultron.prototype.once = function once(event, fn, context) {
|
||||
fn.__ultron = this.id;
|
||||
this.ee.once(event, fn, context);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the listeners we assigned for the given event.
|
||||
*
|
||||
* @returns {Ultron}
|
||||
* @api public
|
||||
*/
|
||||
Ultron.prototype.remove = function remove() {
|
||||
var args = arguments
|
||||
, event;
|
||||
|
||||
//
|
||||
// When no event names are provided we assume that we need to clear all the
|
||||
// events that were assigned through us.
|
||||
//
|
||||
if (args.length === 1 && 'string' === typeof args[0]) {
|
||||
args = args[0].split(/[, ]+/);
|
||||
} else if (!args.length) {
|
||||
args = [];
|
||||
|
||||
for (event in this.ee._events) {
|
||||
if (has.call(this.ee._events, event)) args.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
var listeners = this.ee.listeners(args[i]);
|
||||
|
||||
for (var j = 0; j < listeners.length; j++) {
|
||||
event = listeners[j];
|
||||
|
||||
//
|
||||
// Once listeners have a `listener` property that stores the real listener
|
||||
// in the EventEmitter that ships with Node.js.
|
||||
//
|
||||
if (event.listener) {
|
||||
if (event.listener.__ultron !== this.id) continue;
|
||||
delete event.listener.__ultron;
|
||||
} else {
|
||||
if (event.__ultron !== this.id) continue;
|
||||
delete event.__ultron;
|
||||
}
|
||||
|
||||
this.ee.removeListener(args[i], event);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroy the Ultron instance, remove all listeners and release all references.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* @api public
|
||||
*/
|
||||
Ultron.prototype.destroy = function destroy() {
|
||||
if (!this.ee) return false;
|
||||
|
||||
this.remove();
|
||||
this.ee = null;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
//
|
||||
// Expose the module.
|
||||
//
|
||||
module.exports = Ultron;
|
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"name": "ultron",
|
||||
"version": "1.0.2",
|
||||
"description": "Ultron is high-intelligence robot. It gathers intel so it can start improving upon his rudimentary design",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"100%": "istanbul check-coverage --statements 100 --functions 100 --lines 100 --branches 100",
|
||||
"test": "mocha test.js",
|
||||
"watch": "mocha --watch test.js",
|
||||
"coverage": "istanbul cover ./node_modules/.bin/_mocha -- test.js",
|
||||
"test-travis": "istanbul cover node_modules/.bin/_mocha --report lcovonly -- test.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/unshiftio/ultron.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Ultron",
|
||||
"robot",
|
||||
"gather",
|
||||
"intelligence",
|
||||
"event",
|
||||
"events",
|
||||
"eventemitter",
|
||||
"emitter",
|
||||
"cleanup"
|
||||
],
|
||||
"author": {
|
||||
"name": "Arnout Kazemier"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"assume": "1.2.x",
|
||||
"eventemitter3": "1.1.x",
|
||||
"istanbul": "0.3.x",
|
||||
"mocha": "2.2.x",
|
||||
"pre-commit": "1.0.x"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/unshiftio/ultron/issues"
|
||||
},
|
||||
"homepage": "https://github.com/unshiftio/ultron",
|
||||
"gitHead": "a10482ae98a09120821545456c90c6d60d540f7c",
|
||||
"_id": "ultron@1.0.2",
|
||||
"_shasum": "ace116ab557cd197386a4e88f4685378c8b2e4fa",
|
||||
"_from": "ultron@>=1.0.0 <1.1.0",
|
||||
"_npmVersion": "2.9.1",
|
||||
"_nodeVersion": "0.12.3",
|
||||
"_npmUser": {
|
||||
"name": "3rdeden",
|
||||
"email": "npm@3rd-Eden.com"
|
||||
},
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "unshift",
|
||||
"email": "npm@unshift.io"
|
||||
},
|
||||
{
|
||||
"name": "v1",
|
||||
"email": "info@3rd-Eden.com"
|
||||
},
|
||||
{
|
||||
"name": "3rdeden",
|
||||
"email": "npm@3rd-Eden.com"
|
||||
}
|
||||
],
|
||||
"dist": {
|
||||
"shasum": "ace116ab557cd197386a4e88f4685378c8b2e4fa",
|
||||
"tarball": "http://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz"
|
||||
},
|
||||
"directories": {},
|
||||
"_resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz",
|
||||
"readme": "ERROR: No README data found!"
|
||||
}
|
|
@ -0,0 +1,327 @@
|
|||
/* istanbul ignore next */
|
||||
describe('Ultron', function () {
|
||||
'use strict';
|
||||
|
||||
var EventEmitter = require('eventemitter3')
|
||||
, EE = require('events').EventEmitter
|
||||
, assume = require('assume')
|
||||
, Ultron = require('./')
|
||||
, ultron
|
||||
, ee;
|
||||
|
||||
beforeEach(function () {
|
||||
ee = new EventEmitter();
|
||||
ultron = new Ultron(ee);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
ultron.destroy();
|
||||
ee.removeAllListeners();
|
||||
});
|
||||
|
||||
it('is exposed as a function', function () {
|
||||
assume(Ultron).is.a('function');
|
||||
});
|
||||
|
||||
it('can be initialized without the new keyword', function () {
|
||||
assume(Ultron(ee)).is.instanceOf(Ultron);
|
||||
});
|
||||
|
||||
it('assigns a unique id to every instance', function () {
|
||||
for (var i = 0; i < 100; i++) {
|
||||
assume(ultron.id).does.not.equal((new Ultron()).id);
|
||||
}
|
||||
});
|
||||
|
||||
it('allows removal through the event emitter', function () {
|
||||
function foo() {}
|
||||
function bar() {}
|
||||
|
||||
ultron.on('foo', foo);
|
||||
ultron.once('foo', bar);
|
||||
|
||||
assume(foo.__ultron).equals(ultron.id);
|
||||
assume(bar.__ultron).equals(ultron.id);
|
||||
assume(ee.listeners('foo').length).equals(2);
|
||||
|
||||
ee.removeListener('foo', foo);
|
||||
assume(ee.listeners('foo').length).equals(1);
|
||||
|
||||
ee.removeListener('foo', bar);
|
||||
assume(ee.listeners('foo').length).equals(0);
|
||||
});
|
||||
|
||||
describe('#on', function () {
|
||||
it('assigns a listener', function () {
|
||||
assume(ee.listeners('foo').length).equals(0);
|
||||
|
||||
function foo() {}
|
||||
|
||||
ultron.on('foo', foo);
|
||||
assume(ee.listeners('foo').length).equals(1);
|
||||
assume(ee.listeners('foo')[0]).equals(foo);
|
||||
});
|
||||
|
||||
it('tags the assigned function', function () {
|
||||
assume(ee.listeners('foo').length).equals(0);
|
||||
|
||||
ultron.on('foo', function () {});
|
||||
assume(ee.listeners('foo')[0].__ultron).equals(ultron.id);
|
||||
});
|
||||
|
||||
it('also passes in the context', function (next) {
|
||||
var context = 1313;
|
||||
|
||||
ultron.on('foo', function (a, b, c) {
|
||||
assume(a).equals('a');
|
||||
assume(b).equals('b');
|
||||
assume(c).equals('c');
|
||||
|
||||
assume(this).equals(context);
|
||||
|
||||
next();
|
||||
}, context);
|
||||
|
||||
ee.emit('foo', 'a', 'b', 'c');
|
||||
});
|
||||
|
||||
it('works with regular eventemitters as well', function (next) {
|
||||
var ee = new EE()
|
||||
, ultron = new Ultron(ee);
|
||||
|
||||
ultron.on('foo', function (a, b, c) {
|
||||
assume(a).equals('a');
|
||||
assume(b).equals('b');
|
||||
assume(c).equals('c');
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
ee.emit('foo', 'a', 'b', 'c');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#once', function () {
|
||||
it('assigns a listener', function () {
|
||||
assume(ee.listeners('foo').length).equals(0);
|
||||
|
||||
function foo() {}
|
||||
ultron.once('foo', foo);
|
||||
assume(ee.listeners('foo').length).equals(1);
|
||||
assume(ee.listeners('foo')[0]).equals(foo);
|
||||
});
|
||||
|
||||
it('tags the assigned function', function () {
|
||||
assume(ee.listeners('foo').length).equals(0);
|
||||
|
||||
ultron.once('foo', function () {});
|
||||
assume(ee.listeners('foo')[0].__ultron).equals(ultron.id);
|
||||
});
|
||||
|
||||
it('also passes in the context', function (next) {
|
||||
var context = 1313;
|
||||
|
||||
ultron.once('foo', function (a, b, c) {
|
||||
assume(a).equals('a');
|
||||
assume(b).equals('b');
|
||||
assume(c).equals('c');
|
||||
|
||||
assume(this).equals(context);
|
||||
|
||||
next();
|
||||
}, context);
|
||||
|
||||
ee.emit('foo', 'a', 'b', 'c');
|
||||
ee.emit('foo', 'a', 'b', 'c'); // Ensure that we don't double execute
|
||||
});
|
||||
|
||||
it('works with regular eventemitters as well', function (next) {
|
||||
var ee = new EE()
|
||||
, ultron = new Ultron(ee);
|
||||
|
||||
ultron.once('foo', function (a, b, c) {
|
||||
assume(a).equals('a');
|
||||
assume(b).equals('b');
|
||||
assume(c).equals('c');
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
ee.emit('foo', 'a', 'b', 'c');
|
||||
ee.emit('foo', 'a', 'b', 'c'); // Ensure that we don't double execute
|
||||
});
|
||||
});
|
||||
|
||||
describe('#remove', function () {
|
||||
it('removes only our assigned `on` listeners', function () {
|
||||
function foo() {}
|
||||
function bar() {}
|
||||
|
||||
ee.on('foo', foo);
|
||||
ultron.on('foo', bar);
|
||||
assume(ee.listeners('foo').length).equals(2);
|
||||
|
||||
ultron.remove('foo');
|
||||
assume(ee.listeners('foo').length).equals(1);
|
||||
assume(ee.listeners('foo')[0]).equals(foo);
|
||||
});
|
||||
|
||||
it('removes our private __ultron references', function () {
|
||||
function once() {}
|
||||
function on() {}
|
||||
|
||||
assume('__ultron' in once).is.false();
|
||||
assume('__ultron' in on).is.false();
|
||||
|
||||
ultron.on('foo', on);
|
||||
ultron.once('bar', once);
|
||||
|
||||
assume('__ultron' in once).is.true();
|
||||
assume('__ultron' in on).is.true();
|
||||
|
||||
ultron.remove('foo, bar');
|
||||
|
||||
assume('__ultron' in once).is.false();
|
||||
assume('__ultron' in on).is.false();
|
||||
|
||||
ultron.destroy();
|
||||
|
||||
ee = new EE();
|
||||
ultron = new Ultron(ee);
|
||||
|
||||
assume('__ultron' in once).is.false();
|
||||
assume('__ultron' in on).is.false();
|
||||
|
||||
ultron.on('foo', on);
|
||||
ultron.once('bar', once);
|
||||
|
||||
assume('__ultron' in once).is.true();
|
||||
assume('__ultron' in on).is.true();
|
||||
|
||||
ultron.remove('foo, bar');
|
||||
|
||||
assume('__ultron' in once).is.false();
|
||||
assume('__ultron' in on).is.false();
|
||||
});
|
||||
|
||||
it('removes only our assigned `once` listeners', function () {
|
||||
function foo() {}
|
||||
function bar() {}
|
||||
|
||||
ee.once('foo', foo);
|
||||
ultron.once('foo', bar);
|
||||
assume(ee.listeners('foo').length).equals(2);
|
||||
|
||||
ultron.remove('foo');
|
||||
assume(ee.listeners('foo').length).equals(1);
|
||||
assume(ee.listeners('foo')[0]).equals(foo);
|
||||
});
|
||||
|
||||
it('removes only our assigned `once` listeners from regular EE', function () {
|
||||
var ee = new EE()
|
||||
, ultron = new Ultron(ee);
|
||||
|
||||
function foo() {}
|
||||
function bar() {}
|
||||
|
||||
ee.once('foo', foo);
|
||||
ultron.once('foo', bar);
|
||||
assume(ee.listeners('foo').length).equals(2);
|
||||
|
||||
ultron.remove('foo');
|
||||
assume(ee.listeners('foo').length).equals(1);
|
||||
assume(ee.listeners('foo')[0].listener).equals(foo);
|
||||
});
|
||||
|
||||
it('removes all assigned events if called without args', function () {
|
||||
function foo() {}
|
||||
function bar() {}
|
||||
|
||||
ultron.on('foo', foo);
|
||||
ultron.on('bar', bar);
|
||||
|
||||
assume(ee.listeners('foo').length).equals(1);
|
||||
assume(ee.listeners('bar').length).equals(1);
|
||||
|
||||
ultron.remove();
|
||||
|
||||
assume(ee.listeners('foo').length).equals(0);
|
||||
assume(ee.listeners('bar').length).equals(0);
|
||||
});
|
||||
|
||||
it('removes multiple listeners based on args', function () {
|
||||
function foo() {}
|
||||
function bar() {}
|
||||
function baz() {}
|
||||
|
||||
ultron.on('foo', foo);
|
||||
ultron.on('bar', bar);
|
||||
ultron.on('baz', baz);
|
||||
|
||||
assume(ee.listeners('foo').length).equals(1);
|
||||
assume(ee.listeners('bar').length).equals(1);
|
||||
assume(ee.listeners('baz').length).equals(1);
|
||||
|
||||
ultron.remove('foo', 'bar');
|
||||
|
||||
assume(ee.listeners('foo').length).equals(0);
|
||||
assume(ee.listeners('bar').length).equals(0);
|
||||
assume(ee.listeners('baz').length).equals(1);
|
||||
});
|
||||
|
||||
it('removes multiple listeners if first arg is seperated string', function () {
|
||||
function foo() {}
|
||||
function bar() {}
|
||||
function baz() {}
|
||||
|
||||
ultron.on('foo', foo);
|
||||
ultron.on('bar', bar);
|
||||
ultron.on('baz', baz);
|
||||
|
||||
assume(ee.listeners('foo').length).equals(1);
|
||||
assume(ee.listeners('bar').length).equals(1);
|
||||
assume(ee.listeners('baz').length).equals(1);
|
||||
|
||||
ultron.remove('foo, bar');
|
||||
|
||||
assume(ee.listeners('foo').length).equals(0);
|
||||
assume(ee.listeners('bar').length).equals(0);
|
||||
assume(ee.listeners('baz').length).equals(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#destroy', function () {
|
||||
it('removes all listeners', function () {
|
||||
function foo() {}
|
||||
function bar() {}
|
||||
function baz() {}
|
||||
|
||||
ultron.on('foo', foo);
|
||||
ultron.on('bar', bar);
|
||||
ultron.on('baz', baz);
|
||||
|
||||
assume(ee.listeners('foo').length).equals(1);
|
||||
assume(ee.listeners('bar').length).equals(1);
|
||||
assume(ee.listeners('baz').length).equals(1);
|
||||
|
||||
ultron.destroy();
|
||||
|
||||
assume(ee.listeners('foo').length).equals(0);
|
||||
assume(ee.listeners('bar').length).equals(0);
|
||||
assume(ee.listeners('baz').length).equals(0);
|
||||
});
|
||||
|
||||
it('removes the .ee reference', function () {
|
||||
assume(ultron.ee).equals(ee);
|
||||
ultron.destroy();
|
||||
assume(ultron.ee).equals(null);
|
||||
});
|
||||
|
||||
it('returns booleans for state indication', function () {
|
||||
assume(ultron.destroy()).is.true();
|
||||
assume(ultron.destroy()).is.false();
|
||||
assume(ultron.destroy()).is.false();
|
||||
assume(ultron.destroy()).is.false();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
npm-debug.log
|
||||
node_modules
|
||||
.*.swp
|
||||
.lock-*
|
||||
build
|
||||
|
||||
bench
|
||||
doc
|
||||
examples
|
||||
test
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
language: node_js
|
||||
sudo: false
|
||||
node_js:
|
||||
- "5"
|
||||
- "4"
|
||||
- "0.12"
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- gcc-4.9
|
||||
- g++-4.9
|
||||
before_install:
|
||||
- export CC="gcc-4.9" CXX="g++-4.9"
|
|
@ -0,0 +1,49 @@
|
|||
'use strict';
|
||||
|
||||
/*!
|
||||
* ws: a node.js websocket client
|
||||
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
var WS = module.exports = require('./lib/WebSocket');
|
||||
|
||||
WS.Server = require('./lib/WebSocketServer');
|
||||
WS.Sender = require('./lib/Sender');
|
||||
WS.Receiver = require('./lib/Receiver');
|
||||
|
||||
/**
|
||||
* Create a new WebSocket server.
|
||||
*
|
||||
* @param {Object} options Server options
|
||||
* @param {Function} fn Optional connection listener.
|
||||
* @returns {WS.Server}
|
||||
* @api public
|
||||
*/
|
||||
WS.createServer = function createServer(options, fn) {
|
||||
var server = new WS.Server(options);
|
||||
|
||||
if (typeof fn === 'function') {
|
||||
server.on('connection', fn);
|
||||
}
|
||||
|
||||
return server;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new WebSocket connection.
|
||||
*
|
||||
* @param {String} address The URL/address we need to connect to.
|
||||
* @param {Function} fn Open listener.
|
||||
* @returns {WS}
|
||||
* @api public
|
||||
*/
|
||||
WS.connect = WS.createConnection = function connect(address, fn) {
|
||||
var client = new WS(address);
|
||||
|
||||
if (typeof fn === 'function') {
|
||||
client.on('open', fn);
|
||||
}
|
||||
|
||||
return client;
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
/*!
|
||||
* ws: a node.js websocket client
|
||||
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
var util = require('util');
|
||||
|
||||
function BufferPool(initialSize, growStrategy, shrinkStrategy) {
|
||||
if (this instanceof BufferPool === false) {
|
||||
throw new TypeError("Classes can't be function-called");
|
||||
}
|
||||
|
||||
if (typeof initialSize === 'function') {
|
||||
shrinkStrategy = growStrategy;
|
||||
growStrategy = initialSize;
|
||||
initialSize = 0;
|
||||
}
|
||||
else if (typeof initialSize === 'undefined') {
|
||||
initialSize = 0;
|
||||
}
|
||||
this._growStrategy = (growStrategy || function(db, size) {
|
||||
return db.used + size;
|
||||
}).bind(null, this);
|
||||
this._shrinkStrategy = (shrinkStrategy || function(db) {
|
||||
return initialSize;
|
||||
}).bind(null, this);
|
||||
this._buffer = initialSize ? new Buffer(initialSize) : null;
|
||||
this._offset = 0;
|
||||
this._used = 0;
|
||||
this._changeFactor = 0;
|
||||
this.__defineGetter__('size', function(){
|
||||
return this._buffer == null ? 0 : this._buffer.length;
|
||||
});
|
||||
this.__defineGetter__('used', function(){
|
||||
return this._used;
|
||||
});
|
||||
}
|
||||
|
||||
BufferPool.prototype.get = function(length) {
|
||||
if (this._buffer == null || this._offset + length > this._buffer.length) {
|
||||
var newBuf = new Buffer(this._growStrategy(length));
|
||||
this._buffer = newBuf;
|
||||
this._offset = 0;
|
||||
}
|
||||
this._used += length;
|
||||
var buf = this._buffer.slice(this._offset, this._offset + length);
|
||||
this._offset += length;
|
||||
return buf;
|
||||
}
|
||||
|
||||
BufferPool.prototype.reset = function(forceNewBuffer) {
|
||||
var len = this._shrinkStrategy();
|
||||
if (len < this.size) this._changeFactor -= 1;
|
||||
if (forceNewBuffer || this._changeFactor < -2) {
|
||||
this._changeFactor = 0;
|
||||
this._buffer = len ? new Buffer(len) : null;
|
||||
}
|
||||
this._offset = 0;
|
||||
this._used = 0;
|
||||
}
|
||||
|
||||
module.exports = BufferPool;
|
|
@ -0,0 +1,47 @@
|
|||
/*!
|
||||
* ws: a node.js websocket client
|
||||
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
module.exports.BufferUtil = {
|
||||
merge: function(mergedBuffer, buffers) {
|
||||
var offset = 0;
|
||||
for (var i = 0, l = buffers.length; i < l; ++i) {
|
||||
var buf = buffers[i];
|
||||
buf.copy(mergedBuffer, offset);
|
||||
offset += buf.length;
|
||||
}
|
||||
},
|
||||
mask: function(source, mask, output, offset, length) {
|
||||
var maskNum = mask.readUInt32LE(0, true);
|
||||
var i = 0;
|
||||
for (; i < length - 3; i += 4) {
|
||||
var num = maskNum ^ source.readUInt32LE(i, true);
|
||||
if (num < 0) num = 4294967296 + num;
|
||||
output.writeUInt32LE(num, offset + i, true);
|
||||
}
|
||||
switch (length % 4) {
|
||||
case 3: output[offset + i + 2] = source[i + 2] ^ mask[2];
|
||||
case 2: output[offset + i + 1] = source[i + 1] ^ mask[1];
|
||||
case 1: output[offset + i] = source[i] ^ mask[0];
|
||||
case 0:;
|
||||
}
|
||||
},
|
||||
unmask: function(data, mask) {
|
||||
var maskNum = mask.readUInt32LE(0, true);
|
||||
var length = data.length;
|
||||
var i = 0;
|
||||
for (; i < length - 3; i += 4) {
|
||||
var num = maskNum ^ data.readUInt32LE(i, true);
|
||||
if (num < 0) num = 4294967296 + num;
|
||||
data.writeUInt32LE(num, i, true);
|
||||
}
|
||||
switch (length % 4) {
|
||||
case 3: data[i + 2] = data[i + 2] ^ mask[2];
|
||||
case 2: data[i + 1] = data[i + 1] ^ mask[1];
|
||||
case 1: data[i] = data[i] ^ mask[0];
|
||||
case 0:;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
'use strict';
|
||||
|
||||
/*!
|
||||
* ws: a node.js websocket client
|
||||
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
try {
|
||||
module.exports = require('bufferutil');
|
||||
} catch (e) {
|
||||
module.exports = require('./BufferUtil.fallback');
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*!
|
||||
* ws: a node.js websocket client
|
||||
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
isValidErrorCode: function(code) {
|
||||
return (code >= 1000 && code <= 1011 && code != 1004 && code != 1005 && code != 1006) ||
|
||||
(code >= 3000 && code <= 4999);
|
||||
},
|
||||
1000: 'normal',
|
||||
1001: 'going away',
|
||||
1002: 'protocol error',
|
||||
1003: 'unsupported data',
|
||||
1004: 'reserved',
|
||||
1005: 'reserved for extensions',
|
||||
1006: 'reserved for extensions',
|
||||
1007: 'inconsistent or invalid data',
|
||||
1008: 'policy violation',
|
||||
1009: 'message too big',
|
||||
1010: 'extension handshake missing',
|
||||
1011: 'an unexpected condition prevented the request from being fulfilled',
|
||||
};
|
|
@ -0,0 +1,70 @@
|
|||
|
||||
var util = require('util');
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
*/
|
||||
|
||||
exports.parse = parse;
|
||||
exports.format = format;
|
||||
|
||||
/**
|
||||
* Parse extensions header value
|
||||
*/
|
||||
|
||||
function parse(value) {
|
||||
value = value || '';
|
||||
|
||||
var extensions = {};
|
||||
|
||||
value.split(',').forEach(function(v) {
|
||||
var params = v.split(';');
|
||||
var token = params.shift().trim();
|
||||
var paramsList = extensions[token] = extensions[token] || [];
|
||||
var parsedParams = {};
|
||||
|
||||
params.forEach(function(param) {
|
||||
var parts = param.trim().split('=');
|
||||
var key = parts[0];
|
||||
var value = parts[1];
|
||||
if (typeof value === 'undefined') {
|
||||
value = true;
|
||||
} else {
|
||||
// unquote value
|
||||
if (value[0] === '"') {
|
||||
value = value.slice(1);
|
||||
}
|
||||
if (value[value.length - 1] === '"') {
|
||||
value = value.slice(0, value.length - 1);
|
||||
}
|
||||
}
|
||||
(parsedParams[key] = parsedParams[key] || []).push(value);
|
||||
});
|
||||
|
||||
paramsList.push(parsedParams);
|
||||
});
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format extensions header value
|
||||
*/
|
||||
|
||||
function format(value) {
|
||||
return Object.keys(value).map(function(token) {
|
||||
var paramsList = value[token];
|
||||
if (!util.isArray(paramsList)) {
|
||||
paramsList = [paramsList];
|
||||
}
|
||||
return paramsList.map(function(params) {
|
||||
return [token].concat(Object.keys(params).map(function(k) {
|
||||
var p = params[k];
|
||||
if (!util.isArray(p)) p = [p];
|
||||
return p.map(function(v) {
|
||||
return v === true ? k : k + '=' + v;
|
||||
}).join('; ');
|
||||
})).join('; ');
|
||||
}).join(', ');
|
||||
}).join(', ');
|
||||
}
|
|
@ -0,0 +1,325 @@
|
|||
|
||||
var zlib = require('zlib');
|
||||
|
||||
var AVAILABLE_WINDOW_BITS = [8, 9, 10, 11, 12, 13, 14, 15];
|
||||
var DEFAULT_WINDOW_BITS = 15;
|
||||
var DEFAULT_MEM_LEVEL = 8;
|
||||
|
||||
PerMessageDeflate.extensionName = 'permessage-deflate';
|
||||
|
||||
/**
|
||||
* Per-message Compression Extensions implementation
|
||||
*/
|
||||
|
||||
function PerMessageDeflate(options, isServer) {
|
||||
if (this instanceof PerMessageDeflate === false) {
|
||||
throw new TypeError("Classes can't be function-called");
|
||||
}
|
||||
|
||||
this._options = options || {};
|
||||
this._isServer = !!isServer;
|
||||
this._inflate = null;
|
||||
this._deflate = null;
|
||||
this.params = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create extension parameters offer
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
PerMessageDeflate.prototype.offer = function() {
|
||||
var params = {};
|
||||
if (this._options.serverNoContextTakeover) {
|
||||
params.server_no_context_takeover = true;
|
||||
}
|
||||
if (this._options.clientNoContextTakeover) {
|
||||
params.client_no_context_takeover = true;
|
||||
}
|
||||
if (this._options.serverMaxWindowBits) {
|
||||
params.server_max_window_bits = this._options.serverMaxWindowBits;
|
||||
}
|
||||
if (this._options.clientMaxWindowBits) {
|
||||
params.client_max_window_bits = this._options.clientMaxWindowBits;
|
||||
} else if (this._options.clientMaxWindowBits == null) {
|
||||
params.client_max_window_bits = true;
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
/**
|
||||
* Accept extension offer
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
PerMessageDeflate.prototype.accept = function(paramsList) {
|
||||
paramsList = this.normalizeParams(paramsList);
|
||||
|
||||
var params;
|
||||
if (this._isServer) {
|
||||
params = this.acceptAsServer(paramsList);
|
||||
} else {
|
||||
params = this.acceptAsClient(paramsList);
|
||||
}
|
||||
|
||||
this.params = params;
|
||||
return params;
|
||||
};
|
||||
|
||||
/**
|
||||
* Releases all resources used by the extension
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
PerMessageDeflate.prototype.cleanup = function() {
|
||||
if (this._inflate) {
|
||||
if (this._inflate.writeInProgress) {
|
||||
this._inflate.pendingClose = true;
|
||||
} else {
|
||||
if (this._inflate.close) this._inflate.close();
|
||||
this._inflate = null;
|
||||
}
|
||||
}
|
||||
if (this._deflate) {
|
||||
if (this._deflate.writeInProgress) {
|
||||
this._deflate.pendingClose = true;
|
||||
} else {
|
||||
if (this._deflate.close) this._deflate.close();
|
||||
this._deflate = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Accept extension offer from client
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
PerMessageDeflate.prototype.acceptAsServer = function(paramsList) {
|
||||
var accepted = {};
|
||||
var result = paramsList.some(function(params) {
|
||||
accepted = {};
|
||||
if (this._options.serverNoContextTakeover === false && params.server_no_context_takeover) {
|
||||
return;
|
||||
}
|
||||
if (this._options.serverMaxWindowBits === false && params.server_max_window_bits) {
|
||||
return;
|
||||
}
|
||||
if (typeof this._options.serverMaxWindowBits === 'number' &&
|
||||
typeof params.server_max_window_bits === 'number' &&
|
||||
this._options.serverMaxWindowBits > params.server_max_window_bits) {
|
||||
return;
|
||||
}
|
||||
if (typeof this._options.clientMaxWindowBits === 'number' && !params.client_max_window_bits) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._options.serverNoContextTakeover || params.server_no_context_takeover) {
|
||||
accepted.server_no_context_takeover = true;
|
||||
}
|
||||
if (this._options.clientNoContextTakeover) {
|
||||
accepted.client_no_context_takeover = true;
|
||||
}
|
||||
if (this._options.clientNoContextTakeover !== false && params.client_no_context_takeover) {
|
||||
accepted.client_no_context_takeover = true;
|
||||
}
|
||||
if (typeof this._options.serverMaxWindowBits === 'number') {
|
||||
accepted.server_max_window_bits = this._options.serverMaxWindowBits;
|
||||
} else if (typeof params.server_max_window_bits === 'number') {
|
||||
accepted.server_max_window_bits = params.server_max_window_bits;
|
||||
}
|
||||
if (typeof this._options.clientMaxWindowBits === 'number') {
|
||||
accepted.client_max_window_bits = this._options.clientMaxWindowBits;
|
||||
} else if (this._options.clientMaxWindowBits !== false && typeof params.client_max_window_bits === 'number') {
|
||||
accepted.client_max_window_bits = params.client_max_window_bits;
|
||||
}
|
||||
return true;
|
||||
}, this);
|
||||
|
||||
if (!result) {
|
||||
throw new Error('Doesn\'t support the offered configuration');
|
||||
}
|
||||
|
||||
return accepted;
|
||||
};
|
||||
|
||||
/**
|
||||
* Accept extension response from server
|
||||
*
|
||||
* @api privaye
|
||||
*/
|
||||
|
||||
PerMessageDeflate.prototype.acceptAsClient = function(paramsList) {
|
||||
var params = paramsList[0];
|
||||
if (this._options.clientNoContextTakeover != null) {
|
||||
if (this._options.clientNoContextTakeover === false && params.client_no_context_takeover) {
|
||||
throw new Error('Invalid value for "client_no_context_takeover"');
|
||||
}
|
||||
}
|
||||
if (this._options.clientMaxWindowBits != null) {
|
||||
if (this._options.clientMaxWindowBits === false && params.client_max_window_bits) {
|
||||
throw new Error('Invalid value for "client_max_window_bits"');
|
||||
}
|
||||
if (typeof this._options.clientMaxWindowBits === 'number' &&
|
||||
(!params.client_max_window_bits || params.client_max_window_bits > this._options.clientMaxWindowBits)) {
|
||||
throw new Error('Invalid value for "client_max_window_bits"');
|
||||
}
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize extensions parameters
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
PerMessageDeflate.prototype.normalizeParams = function(paramsList) {
|
||||
return paramsList.map(function(params) {
|
||||
Object.keys(params).forEach(function(key) {
|
||||
var value = params[key];
|
||||
if (value.length > 1) {
|
||||
throw new Error('Multiple extension parameters for ' + key);
|
||||
}
|
||||
|
||||
value = value[0];
|
||||
|
||||
switch (key) {
|
||||
case 'server_no_context_takeover':
|
||||
case 'client_no_context_takeover':
|
||||
if (value !== true) {
|
||||
throw new Error('invalid extension parameter value for ' + key + ' (' + value + ')');
|
||||
}
|
||||
params[key] = true;
|
||||
break;
|
||||
case 'server_max_window_bits':
|
||||
case 'client_max_window_bits':
|
||||
if (typeof value === 'string') {
|
||||
value = parseInt(value, 10);
|
||||
if (!~AVAILABLE_WINDOW_BITS.indexOf(value)) {
|
||||
throw new Error('invalid extension parameter value for ' + key + ' (' + value + ')');
|
||||
}
|
||||
}
|
||||
if (!this._isServer && value === true) {
|
||||
throw new Error('Missing extension parameter value for ' + key);
|
||||
}
|
||||
params[key] = value;
|
||||
break;
|
||||
default:
|
||||
throw new Error('Not defined extension parameter (' + key + ')');
|
||||
}
|
||||
}, this);
|
||||
return params;
|
||||
}, this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Decompress message
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
PerMessageDeflate.prototype.decompress = function (data, fin, callback) {
|
||||
var endpoint = this._isServer ? 'client' : 'server';
|
||||
|
||||
if (!this._inflate) {
|
||||
var maxWindowBits = this.params[endpoint + '_max_window_bits'];
|
||||
this._inflate = zlib.createInflateRaw({
|
||||
windowBits: 'number' === typeof maxWindowBits ? maxWindowBits : DEFAULT_WINDOW_BITS
|
||||
});
|
||||
}
|
||||
this._inflate.writeInProgress = true;
|
||||
|
||||
var self = this;
|
||||
var buffers = [];
|
||||
|
||||
this._inflate.on('error', onError).on('data', onData);
|
||||
this._inflate.write(data);
|
||||
if (fin) {
|
||||
this._inflate.write(new Buffer([0x00, 0x00, 0xff, 0xff]));
|
||||
}
|
||||
this._inflate.flush(function() {
|
||||
cleanup();
|
||||
callback(null, Buffer.concat(buffers));
|
||||
});
|
||||
|
||||
function onError(err) {
|
||||
cleanup();
|
||||
callback(err);
|
||||
}
|
||||
|
||||
function onData(data) {
|
||||
buffers.push(data);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (!self._inflate) return;
|
||||
self._inflate.removeListener('error', onError);
|
||||
self._inflate.removeListener('data', onData);
|
||||
self._inflate.writeInProgress = false;
|
||||
if ((fin && self.params[endpoint + '_no_context_takeover']) || self._inflate.pendingClose) {
|
||||
if (self._inflate.close) self._inflate.close();
|
||||
self._inflate = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Compress message
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
PerMessageDeflate.prototype.compress = function (data, fin, callback) {
|
||||
var endpoint = this._isServer ? 'server' : 'client';
|
||||
|
||||
if (!this._deflate) {
|
||||
var maxWindowBits = this.params[endpoint + '_max_window_bits'];
|
||||
this._deflate = zlib.createDeflateRaw({
|
||||
flush: zlib.Z_SYNC_FLUSH,
|
||||
windowBits: 'number' === typeof maxWindowBits ? maxWindowBits : DEFAULT_WINDOW_BITS,
|
||||
memLevel: this._options.memLevel || DEFAULT_MEM_LEVEL
|
||||
});
|
||||
}
|
||||
this._deflate.writeInProgress = true;
|
||||
|
||||
var self = this;
|
||||
var buffers = [];
|
||||
|
||||
this._deflate.on('error', onError).on('data', onData);
|
||||
this._deflate.write(data);
|
||||
this._deflate.flush(function() {
|
||||
cleanup();
|
||||
var data = Buffer.concat(buffers);
|
||||
if (fin) {
|
||||
data = data.slice(0, data.length - 4);
|
||||
}
|
||||
callback(null, data);
|
||||
});
|
||||
|
||||
function onError(err) {
|
||||
cleanup();
|
||||
callback(err);
|
||||
}
|
||||
|
||||
function onData(data) {
|
||||
buffers.push(data);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (!self._deflate) return;
|
||||
self._deflate.removeListener('error', onError);
|
||||
self._deflate.removeListener('data', onData);
|
||||
self._deflate.writeInProgress = false;
|
||||
if ((fin && self.params[endpoint + '_no_context_takeover']) || self._deflate.pendingClose) {
|
||||
if (self._deflate.close) self._deflate.close();
|
||||
self._deflate = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = PerMessageDeflate;
|
|
@ -0,0 +1,184 @@
|
|||
/*!
|
||||
* ws: a node.js websocket client
|
||||
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
var util = require('util');
|
||||
|
||||
/**
|
||||
* State constants
|
||||
*/
|
||||
|
||||
var EMPTY = 0
|
||||
, BODY = 1;
|
||||
var BINARYLENGTH = 2
|
||||
, BINARYBODY = 3;
|
||||
|
||||
/**
|
||||
* Hixie Receiver implementation
|
||||
*/
|
||||
|
||||
function Receiver () {
|
||||
if (this instanceof Receiver === false) {
|
||||
throw new TypeError("Classes can't be function-called");
|
||||
}
|
||||
|
||||
this.state = EMPTY;
|
||||
this.buffers = [];
|
||||
this.messageEnd = -1;
|
||||
this.spanLength = 0;
|
||||
this.dead = false;
|
||||
|
||||
this.onerror = function() {};
|
||||
this.ontext = function() {};
|
||||
this.onbinary = function() {};
|
||||
this.onclose = function() {};
|
||||
this.onping = function() {};
|
||||
this.onpong = function() {};
|
||||
}
|
||||
|
||||
module.exports = Receiver;
|
||||
|
||||
/**
|
||||
* Add new data to the parser.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Receiver.prototype.add = function(data) {
|
||||
var self = this;
|
||||
function doAdd() {
|
||||
if (self.state === EMPTY) {
|
||||
if (data.length == 2 && data[0] == 0xFF && data[1] == 0x00) {
|
||||
self.reset();
|
||||
self.onclose();
|
||||
return;
|
||||
}
|
||||
if (data[0] === 0x80) {
|
||||
self.messageEnd = 0;
|
||||
self.state = BINARYLENGTH;
|
||||
data = data.slice(1);
|
||||
} else {
|
||||
|
||||
if (data[0] !== 0x00) {
|
||||
self.error('payload must start with 0x00 byte', true);
|
||||
return;
|
||||
}
|
||||
data = data.slice(1);
|
||||
self.state = BODY;
|
||||
|
||||
}
|
||||
}
|
||||
if (self.state === BINARYLENGTH) {
|
||||
var i = 0;
|
||||
while ((i < data.length) && (data[i] & 0x80)) {
|
||||
self.messageEnd = 128 * self.messageEnd + (data[i] & 0x7f);
|
||||
++i;
|
||||
}
|
||||
if (i < data.length) {
|
||||
self.messageEnd = 128 * self.messageEnd + (data[i] & 0x7f);
|
||||
self.state = BINARYBODY;
|
||||
++i;
|
||||
}
|
||||
if (i > 0)
|
||||
data = data.slice(i);
|
||||
}
|
||||
if (self.state === BINARYBODY) {
|
||||
var dataleft = self.messageEnd - self.spanLength;
|
||||
if (data.length >= dataleft) {
|
||||
// consume the whole buffer to finish the frame
|
||||
self.buffers.push(data);
|
||||
self.spanLength += dataleft;
|
||||
self.messageEnd = dataleft;
|
||||
return self.parse();
|
||||
}
|
||||
// frame's not done even if we consume it all
|
||||
self.buffers.push(data);
|
||||
self.spanLength += data.length;
|
||||
return;
|
||||
}
|
||||
self.buffers.push(data);
|
||||
if ((self.messageEnd = bufferIndex(data, 0xFF)) != -1) {
|
||||
self.spanLength += self.messageEnd;
|
||||
return self.parse();
|
||||
}
|
||||
else self.spanLength += data.length;
|
||||
}
|
||||
while(data) data = doAdd();
|
||||
};
|
||||
|
||||
/**
|
||||
* Releases all resources used by the receiver.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Receiver.prototype.cleanup = function() {
|
||||
this.dead = true;
|
||||
this.state = EMPTY;
|
||||
this.buffers = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Process buffered data.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Receiver.prototype.parse = function() {
|
||||
var output = new Buffer(this.spanLength);
|
||||
var outputIndex = 0;
|
||||
for (var bi = 0, bl = this.buffers.length; bi < bl - 1; ++bi) {
|
||||
var buffer = this.buffers[bi];
|
||||
buffer.copy(output, outputIndex);
|
||||
outputIndex += buffer.length;
|
||||
}
|
||||
var lastBuffer = this.buffers[this.buffers.length - 1];
|
||||
if (this.messageEnd > 0) lastBuffer.copy(output, outputIndex, 0, this.messageEnd);
|
||||
if (this.state !== BODY) --this.messageEnd;
|
||||
var tail = null;
|
||||
if (this.messageEnd < lastBuffer.length - 1) {
|
||||
tail = lastBuffer.slice(this.messageEnd + 1);
|
||||
}
|
||||
this.reset();
|
||||
this.ontext(output.toString('utf8'));
|
||||
return tail;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles an error
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Receiver.prototype.error = function (reason, terminate) {
|
||||
this.reset();
|
||||
this.onerror(reason, terminate);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset parser state
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Receiver.prototype.reset = function (reason) {
|
||||
if (this.dead) return;
|
||||
this.state = EMPTY;
|
||||
this.buffers = [];
|
||||
this.messageEnd = -1;
|
||||
this.spanLength = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal api
|
||||
*/
|
||||
|
||||
function bufferIndex(buffer, byte) {
|
||||
for (var i = 0, l = buffer.length; i < l; ++i) {
|
||||
if (buffer[i] === byte) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
|
@ -0,0 +1,702 @@
|
|||
/*!
|
||||
* ws: a node.js websocket client
|
||||
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
var util = require('util')
|
||||
, Validation = require('./Validation').Validation
|
||||
, ErrorCodes = require('./ErrorCodes')
|
||||
, BufferPool = require('./BufferPool')
|
||||
, bufferUtil = require('./BufferUtil').BufferUtil
|
||||
, PerMessageDeflate = require('./PerMessageDeflate');
|
||||
|
||||
/**
|
||||
* HyBi Receiver implementation
|
||||
*/
|
||||
|
||||
function Receiver (extensions) {
|
||||
if (this instanceof Receiver === false) {
|
||||
throw new TypeError("Classes can't be function-called");
|
||||
}
|
||||
|
||||
// memory pool for fragmented messages
|
||||
var fragmentedPoolPrevUsed = -1;
|
||||
this.fragmentedBufferPool = new BufferPool(1024, function(db, length) {
|
||||
return db.used + length;
|
||||
}, function(db) {
|
||||
return fragmentedPoolPrevUsed = fragmentedPoolPrevUsed >= 0 ?
|
||||
Math.ceil((fragmentedPoolPrevUsed + db.used) / 2) :
|
||||
db.used;
|
||||
});
|
||||
|
||||
// memory pool for unfragmented messages
|
||||
var unfragmentedPoolPrevUsed = -1;
|
||||
this.unfragmentedBufferPool = new BufferPool(1024, function(db, length) {
|
||||
return db.used + length;
|
||||
}, function(db) {
|
||||
return unfragmentedPoolPrevUsed = unfragmentedPoolPrevUsed >= 0 ?
|
||||
Math.ceil((unfragmentedPoolPrevUsed + db.used) / 2) :
|
||||
db.used;
|
||||
});
|
||||
|
||||
this.extensions = extensions || {};
|
||||
this.state = {
|
||||
activeFragmentedOperation: null,
|
||||
lastFragment: false,
|
||||
masked: false,
|
||||
opcode: 0,
|
||||
fragmentedOperation: false
|
||||
};
|
||||
this.overflow = [];
|
||||
this.headerBuffer = new Buffer(10);
|
||||
this.expectOffset = 0;
|
||||
this.expectBuffer = null;
|
||||
this.expectHandler = null;
|
||||
this.currentMessage = [];
|
||||
this.messageHandlers = [];
|
||||
this.expectHeader(2, this.processPacket);
|
||||
this.dead = false;
|
||||
this.processing = false;
|
||||
|
||||
this.onerror = function() {};
|
||||
this.ontext = function() {};
|
||||
this.onbinary = function() {};
|
||||
this.onclose = function() {};
|
||||
this.onping = function() {};
|
||||
this.onpong = function() {};
|
||||
}
|
||||
|
||||
module.exports = Receiver;
|
||||
|
||||
/**
|
||||
* Add new data to the parser.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Receiver.prototype.add = function(data) {
|
||||
var dataLength = data.length;
|
||||
if (dataLength == 0) return;
|
||||
if (this.expectBuffer == null) {
|
||||
this.overflow.push(data);
|
||||
return;
|
||||
}
|
||||
var toRead = Math.min(dataLength, this.expectBuffer.length - this.expectOffset);
|
||||
fastCopy(toRead, data, this.expectBuffer, this.expectOffset);
|
||||
this.expectOffset += toRead;
|
||||
if (toRead < dataLength) {
|
||||
this.overflow.push(data.slice(toRead));
|
||||
}
|
||||
while (this.expectBuffer && this.expectOffset == this.expectBuffer.length) {
|
||||
var bufferForHandler = this.expectBuffer;
|
||||
this.expectBuffer = null;
|
||||
this.expectOffset = 0;
|
||||
this.expectHandler.call(this, bufferForHandler);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Releases all resources used by the receiver.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Receiver.prototype.cleanup = function() {
|
||||
this.dead = true;
|
||||
this.overflow = null;
|
||||
this.headerBuffer = null;
|
||||
this.expectBuffer = null;
|
||||
this.expectHandler = null;
|
||||
this.unfragmentedBufferPool = null;
|
||||
this.fragmentedBufferPool = null;
|
||||
this.state = null;
|
||||
this.currentMessage = null;
|
||||
this.onerror = null;
|
||||
this.ontext = null;
|
||||
this.onbinary = null;
|
||||
this.onclose = null;
|
||||
this.onping = null;
|
||||
this.onpong = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Waits for a certain amount of header bytes to be available, then fires a callback.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Receiver.prototype.expectHeader = function(length, handler) {
|
||||
if (length == 0) {
|
||||
handler(null);
|
||||
return;
|
||||
}
|
||||
this.expectBuffer = this.headerBuffer.slice(this.expectOffset, this.expectOffset + length);
|
||||
this.expectHandler = handler;
|
||||
var toRead = length;
|
||||
while (toRead > 0 && this.overflow.length > 0) {
|
||||
var fromOverflow = this.overflow.pop();
|
||||
if (toRead < fromOverflow.length) this.overflow.push(fromOverflow.slice(toRead));
|
||||
var read = Math.min(fromOverflow.length, toRead);
|
||||
fastCopy(read, fromOverflow, this.expectBuffer, this.expectOffset);
|
||||
this.expectOffset += read;
|
||||
toRead -= read;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Waits for a certain amount of data bytes to be available, then fires a callback.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Receiver.prototype.expectData = function(length, handler) {
|
||||
if (length == 0) {
|
||||
handler(null);
|
||||
return;
|
||||
}
|
||||
this.expectBuffer = this.allocateFromPool(length, this.state.fragmentedOperation);
|
||||
this.expectHandler = handler;
|
||||
var toRead = length;
|
||||
while (toRead > 0 && this.overflow.length > 0) {
|
||||
var fromOverflow = this.overflow.pop();
|
||||
if (toRead < fromOverflow.length) this.overflow.push(fromOverflow.slice(toRead));
|
||||
var read = Math.min(fromOverflow.length, toRead);
|
||||
fastCopy(read, fromOverflow, this.expectBuffer, this.expectOffset);
|
||||
this.expectOffset += read;
|
||||
toRead -= read;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Allocates memory from the buffer pool.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Receiver.prototype.allocateFromPool = function(length, isFragmented) {
|
||||
return (isFragmented ? this.fragmentedBufferPool : this.unfragmentedBufferPool).get(length);
|
||||
};
|
||||
|
||||
/**
|
||||
* Start processing a new packet.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Receiver.prototype.processPacket = function (data) {
|
||||
if (this.extensions[PerMessageDeflate.extensionName]) {
|
||||
if ((data[0] & 0x30) != 0) {
|
||||
this.error('reserved fields (2, 3) must be empty', 1002);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if ((data[0] & 0x70) != 0) {
|
||||
this.error('reserved fields must be empty', 1002);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.state.lastFragment = (data[0] & 0x80) == 0x80;
|
||||
this.state.masked = (data[1] & 0x80) == 0x80;
|
||||
var compressed = (data[0] & 0x40) == 0x40;
|
||||
var opcode = data[0] & 0xf;
|
||||
if (opcode === 0) {
|
||||
if (compressed) {
|
||||
this.error('continuation frame cannot have the Per-message Compressed bits', 1002);
|
||||
return;
|
||||
}
|
||||
// continuation frame
|
||||
this.state.fragmentedOperation = true;
|
||||
this.state.opcode = this.state.activeFragmentedOperation;
|
||||
if (!(this.state.opcode == 1 || this.state.opcode == 2)) {
|
||||
this.error('continuation frame cannot follow current opcode', 1002);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (opcode < 3 && this.state.activeFragmentedOperation != null) {
|
||||
this.error('data frames after the initial data frame must have opcode 0', 1002);
|
||||
return;
|
||||
}
|
||||
if (opcode >= 8 && compressed) {
|
||||
this.error('control frames cannot have the Per-message Compressed bits', 1002);
|
||||
return;
|
||||
}
|
||||
this.state.compressed = compressed;
|
||||
this.state.opcode = opcode;
|
||||
if (this.state.lastFragment === false) {
|
||||
this.state.fragmentedOperation = true;
|
||||
this.state.activeFragmentedOperation = opcode;
|
||||
}
|
||||
else this.state.fragmentedOperation = false;
|
||||
}
|
||||
var handler = opcodes[this.state.opcode];
|
||||
if (typeof handler == 'undefined') this.error('no handler for opcode ' + this.state.opcode, 1002);
|
||||
else {
|
||||
handler.start.call(this, data);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Endprocessing a packet.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Receiver.prototype.endPacket = function() {
|
||||
if (!this.state.fragmentedOperation) this.unfragmentedBufferPool.reset(true);
|
||||
else if (this.state.lastFragment) this.fragmentedBufferPool.reset(true);
|
||||
this.expectOffset = 0;
|
||||
this.expectBuffer = null;
|
||||
this.expectHandler = null;
|
||||
if (this.state.lastFragment && this.state.opcode === this.state.activeFragmentedOperation) {
|
||||
// end current fragmented operation
|
||||
this.state.activeFragmentedOperation = null;
|
||||
}
|
||||
this.state.lastFragment = false;
|
||||
this.state.opcode = this.state.activeFragmentedOperation != null ? this.state.activeFragmentedOperation : 0;
|
||||
this.state.masked = false;
|
||||
this.expectHeader(2, this.processPacket);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the parser state.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Receiver.prototype.reset = function() {
|
||||
if (this.dead) return;
|
||||
this.state = {
|
||||
activeFragmentedOperation: null,
|
||||
lastFragment: false,
|
||||
masked: false,
|
||||
opcode: 0,
|
||||
fragmentedOperation: false
|
||||
};
|
||||
this.fragmentedBufferPool.reset(true);
|
||||
this.unfragmentedBufferPool.reset(true);
|
||||
this.expectOffset = 0;
|
||||
this.expectBuffer = null;
|
||||
this.expectHandler = null;
|
||||
this.overflow = [];
|
||||
this.currentMessage = [];
|
||||
this.messageHandlers = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Unmask received data.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Receiver.prototype.unmask = function (mask, buf, binary) {
|
||||
if (mask != null && buf != null) bufferUtil.unmask(buf, mask);
|
||||
if (binary) return buf;
|
||||
return buf != null ? buf.toString('utf8') : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Concatenates a list of buffers.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Receiver.prototype.concatBuffers = function(buffers) {
|
||||
var length = 0;
|
||||
for (var i = 0, l = buffers.length; i < l; ++i) length += buffers[i].length;
|
||||
var mergedBuffer = new Buffer(length);
|
||||
bufferUtil.merge(mergedBuffer, buffers);
|
||||
return mergedBuffer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles an error
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Receiver.prototype.error = function (reason, protocolErrorCode) {
|
||||
this.reset();
|
||||
this.onerror(reason, protocolErrorCode);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute message handler buffers
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Receiver.prototype.flush = function() {
|
||||
if (this.processing || this.dead) return;
|
||||
|
||||
var handler = this.messageHandlers.shift();
|
||||
if (!handler) return;
|
||||
|
||||
this.processing = true;
|
||||
var self = this;
|
||||
|
||||
handler(function() {
|
||||
self.processing = false;
|
||||
self.flush();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply extensions to message
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Receiver.prototype.applyExtensions = function(messageBuffer, fin, compressed, callback) {
|
||||
var self = this;
|
||||
if (compressed) {
|
||||
this.extensions[PerMessageDeflate.extensionName].decompress(messageBuffer, fin, function(err, buffer) {
|
||||
if (self.dead) return;
|
||||
if (err) {
|
||||
callback(new Error('invalid compressed data'));
|
||||
return;
|
||||
}
|
||||
callback(null, buffer);
|
||||
});
|
||||
} else {
|
||||
callback(null, messageBuffer);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Buffer utilities
|
||||
*/
|
||||
|
||||
function readUInt16BE(start) {
|
||||
return (this[start]<<8) +
|
||||
this[start+1];
|
||||
}
|
||||
|
||||
function readUInt32BE(start) {
|
||||
return (this[start]<<24) +
|
||||
(this[start+1]<<16) +
|
||||
(this[start+2]<<8) +
|
||||
this[start+3];
|
||||
}
|
||||
|
||||
function fastCopy(length, srcBuffer, dstBuffer, dstOffset) {
|
||||
switch (length) {
|
||||
default: srcBuffer.copy(dstBuffer, dstOffset, 0, length); break;
|
||||
case 16: dstBuffer[dstOffset+15] = srcBuffer[15];
|
||||
case 15: dstBuffer[dstOffset+14] = srcBuffer[14];
|
||||
case 14: dstBuffer[dstOffset+13] = srcBuffer[13];
|
||||
case 13: dstBuffer[dstOffset+12] = srcBuffer[12];
|
||||
case 12: dstBuffer[dstOffset+11] = srcBuffer[11];
|
||||
case 11: dstBuffer[dstOffset+10] = srcBuffer[10];
|
||||
case 10: dstBuffer[dstOffset+9] = srcBuffer[9];
|
||||
case 9: dstBuffer[dstOffset+8] = srcBuffer[8];
|
||||
case 8: dstBuffer[dstOffset+7] = srcBuffer[7];
|
||||
case 7: dstBuffer[dstOffset+6] = srcBuffer[6];
|
||||
case 6: dstBuffer[dstOffset+5] = srcBuffer[5];
|
||||
case 5: dstBuffer[dstOffset+4] = srcBuffer[4];
|
||||
case 4: dstBuffer[dstOffset+3] = srcBuffer[3];
|
||||
case 3: dstBuffer[dstOffset+2] = srcBuffer[2];
|
||||
case 2: dstBuffer[dstOffset+1] = srcBuffer[1];
|
||||
case 1: dstBuffer[dstOffset] = srcBuffer[0];
|
||||
}
|
||||
}
|
||||
|
||||
function clone(obj) {
|
||||
var cloned = {};
|
||||
for (var k in obj) {
|
||||
if (obj.hasOwnProperty(k)) {
|
||||
cloned[k] = obj[k];
|
||||
}
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opcode handlers
|
||||
*/
|
||||
|
||||
var opcodes = {
|
||||
// text
|
||||
'1': {
|
||||
start: function(data) {
|
||||
var self = this;
|
||||
// decode length
|
||||
var firstLength = data[1] & 0x7f;
|
||||
if (firstLength < 126) {
|
||||
opcodes['1'].getData.call(self, firstLength);
|
||||
}
|
||||
else if (firstLength == 126) {
|
||||
self.expectHeader(2, function(data) {
|
||||
opcodes['1'].getData.call(self, readUInt16BE.call(data, 0));
|
||||
});
|
||||
}
|
||||
else if (firstLength == 127) {
|
||||
self.expectHeader(8, function(data) {
|
||||
if (readUInt32BE.call(data, 0) != 0) {
|
||||
self.error('packets with length spanning more than 32 bit is currently not supported', 1008);
|
||||
return;
|
||||
}
|
||||
opcodes['1'].getData.call(self, readUInt32BE.call(data, 4));
|
||||
});
|
||||
}
|
||||
},
|
||||
getData: function(length) {
|
||||
var self = this;
|
||||
if (self.state.masked) {
|
||||
self.expectHeader(4, function(data) {
|
||||
var mask = data;
|
||||
self.expectData(length, function(data) {
|
||||
opcodes['1'].finish.call(self, mask, data);
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
self.expectData(length, function(data) {
|
||||
opcodes['1'].finish.call(self, null, data);
|
||||
});
|
||||
}
|
||||
},
|
||||
finish: function(mask, data) {
|
||||
var self = this;
|
||||
var packet = this.unmask(mask, data, true) || new Buffer(0);
|
||||
var state = clone(this.state);
|
||||
this.messageHandlers.push(function(callback) {
|
||||
self.applyExtensions(packet, state.lastFragment, state.compressed, function(err, buffer) {
|
||||
if (err) return self.error(err.message, 1007);
|
||||
if (buffer != null) self.currentMessage.push(buffer);
|
||||
|
||||
if (state.lastFragment) {
|
||||
var messageBuffer = self.concatBuffers(self.currentMessage);
|
||||
self.currentMessage = [];
|
||||
if (!Validation.isValidUTF8(messageBuffer)) {
|
||||
self.error('invalid utf8 sequence', 1007);
|
||||
return;
|
||||
}
|
||||
self.ontext(messageBuffer.toString('utf8'), {masked: state.masked, buffer: messageBuffer});
|
||||
}
|
||||
callback();
|
||||
});
|
||||
});
|
||||
this.flush();
|
||||
this.endPacket();
|
||||
}
|
||||
},
|
||||
// binary
|
||||
'2': {
|
||||
start: function(data) {
|
||||
var self = this;
|
||||
// decode length
|
||||
var firstLength = data[1] & 0x7f;
|
||||
if (firstLength < 126) {
|
||||
opcodes['2'].getData.call(self, firstLength);
|
||||
}
|
||||
else if (firstLength == 126) {
|
||||
self.expectHeader(2, function(data) {
|
||||
opcodes['2'].getData.call(self, readUInt16BE.call(data, 0));
|
||||
});
|
||||
}
|
||||
else if (firstLength == 127) {
|
||||
self.expectHeader(8, function(data) {
|
||||
if (readUInt32BE.call(data, 0) != 0) {
|
||||
self.error('packets with length spanning more than 32 bit is currently not supported', 1008);
|
||||
return;
|
||||
}
|
||||
opcodes['2'].getData.call(self, readUInt32BE.call(data, 4, true));
|
||||
});
|
||||
}
|
||||
},
|
||||
getData: function(length) {
|
||||
var self = this;
|
||||
if (self.state.masked) {
|
||||
self.expectHeader(4, function(data) {
|
||||
var mask = data;
|
||||
self.expectData(length, function(data) {
|
||||
opcodes['2'].finish.call(self, mask, data);
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
self.expectData(length, function(data) {
|
||||
opcodes['2'].finish.call(self, null, data);
|
||||
});
|
||||
}
|
||||
},
|
||||
finish: function(mask, data) {
|
||||
var self = this;
|
||||
var packet = this.unmask(mask, data, true) || new Buffer(0);
|
||||
var state = clone(this.state);
|
||||
this.messageHandlers.push(function(callback) {
|
||||
self.applyExtensions(packet, state.lastFragment, state.compressed, function(err, buffer) {
|
||||
if (err) return self.error(err.message, 1007);
|
||||
if (buffer != null) self.currentMessage.push(buffer);
|
||||
if (state.lastFragment) {
|
||||
var messageBuffer = self.concatBuffers(self.currentMessage);
|
||||
self.currentMessage = [];
|
||||
self.onbinary(messageBuffer, {masked: state.masked, buffer: messageBuffer});
|
||||
}
|
||||
callback();
|
||||
});
|
||||
});
|
||||
this.flush();
|
||||
this.endPacket();
|
||||
}
|
||||
},
|
||||
// close
|
||||
'8': {
|
||||
start: function(data) {
|
||||
var self = this;
|
||||
if (self.state.lastFragment == false) {
|
||||
self.error('fragmented close is not supported', 1002);
|
||||
return;
|
||||
}
|
||||
|
||||
// decode length
|
||||
var firstLength = data[1] & 0x7f;
|
||||
if (firstLength < 126) {
|
||||
opcodes['8'].getData.call(self, firstLength);
|
||||
}
|
||||
else {
|
||||
self.error('control frames cannot have more than 125 bytes of data', 1002);
|
||||
}
|
||||
},
|
||||
getData: function(length) {
|
||||
var self = this;
|
||||
if (self.state.masked) {
|
||||
self.expectHeader(4, function(data) {
|
||||
var mask = data;
|
||||
self.expectData(length, function(data) {
|
||||
opcodes['8'].finish.call(self, mask, data);
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
self.expectData(length, function(data) {
|
||||
opcodes['8'].finish.call(self, null, data);
|
||||
});
|
||||
}
|
||||
},
|
||||
finish: function(mask, data) {
|
||||
var self = this;
|
||||
data = self.unmask(mask, data, true);
|
||||
|
||||
var state = clone(this.state);
|
||||
this.messageHandlers.push(function() {
|
||||
if (data && data.length == 1) {
|
||||
self.error('close packets with data must be at least two bytes long', 1002);
|
||||
return;
|
||||
}
|
||||
var code = data && data.length > 1 ? readUInt16BE.call(data, 0) : 1000;
|
||||
if (!ErrorCodes.isValidErrorCode(code)) {
|
||||
self.error('invalid error code', 1002);
|
||||
return;
|
||||
}
|
||||
var message = '';
|
||||
if (data && data.length > 2) {
|
||||
var messageBuffer = data.slice(2);
|
||||
if (!Validation.isValidUTF8(messageBuffer)) {
|
||||
self.error('invalid utf8 sequence', 1007);
|
||||
return;
|
||||
}
|
||||
message = messageBuffer.toString('utf8');
|
||||
}
|
||||
self.onclose(code, message, {masked: state.masked});
|
||||
self.reset();
|
||||
});
|
||||
this.flush();
|
||||
},
|
||||
},
|
||||
// ping
|
||||
'9': {
|
||||
start: function(data) {
|
||||
var self = this;
|
||||
if (self.state.lastFragment == false) {
|
||||
self.error('fragmented ping is not supported', 1002);
|
||||
return;
|
||||
}
|
||||
|
||||
// decode length
|
||||
var firstLength = data[1] & 0x7f;
|
||||
if (firstLength < 126) {
|
||||
opcodes['9'].getData.call(self, firstLength);
|
||||
}
|
||||
else {
|
||||
self.error('control frames cannot have more than 125 bytes of data', 1002);
|
||||
}
|
||||
},
|
||||
getData: function(length) {
|
||||
var self = this;
|
||||
if (self.state.masked) {
|
||||
self.expectHeader(4, function(data) {
|
||||
var mask = data;
|
||||
self.expectData(length, function(data) {
|
||||
opcodes['9'].finish.call(self, mask, data);
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
self.expectData(length, function(data) {
|
||||
opcodes['9'].finish.call(self, null, data);
|
||||
});
|
||||
}
|
||||
},
|
||||
finish: function(mask, data) {
|
||||
var self = this;
|
||||
data = this.unmask(mask, data, true);
|
||||
var state = clone(this.state);
|
||||
this.messageHandlers.push(function(callback) {
|
||||
self.onping(data, {masked: state.masked, binary: true});
|
||||
callback();
|
||||
});
|
||||
this.flush();
|
||||
this.endPacket();
|
||||
}
|
||||
},
|
||||
// pong
|
||||
'10': {
|
||||
start: function(data) {
|
||||
var self = this;
|
||||
if (self.state.lastFragment == false) {
|
||||
self.error('fragmented pong is not supported', 1002);
|
||||
return;
|
||||
}
|
||||
|
||||
// decode length
|
||||
var firstLength = data[1] & 0x7f;
|
||||
if (firstLength < 126) {
|
||||
opcodes['10'].getData.call(self, firstLength);
|
||||
}
|
||||
else {
|
||||
self.error('control frames cannot have more than 125 bytes of data', 1002);
|
||||
}
|
||||
},
|
||||
getData: function(length) {
|
||||
var self = this;
|
||||
if (this.state.masked) {
|
||||
this.expectHeader(4, function(data) {
|
||||
var mask = data;
|
||||
self.expectData(length, function(data) {
|
||||
opcodes['10'].finish.call(self, mask, data);
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.expectData(length, function(data) {
|
||||
opcodes['10'].finish.call(self, null, data);
|
||||
});
|
||||
}
|
||||
},
|
||||
finish: function(mask, data) {
|
||||
var self = this;
|
||||
data = self.unmask(mask, data, true);
|
||||
var state = clone(this.state);
|
||||
this.messageHandlers.push(function(callback) {
|
||||
self.onpong(data, {masked: state.masked, binary: true});
|
||||
callback();
|
||||
});
|
||||
this.flush();
|
||||
this.endPacket();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
/*!
|
||||
* ws: a node.js websocket client
|
||||
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
var events = require('events')
|
||||
, util = require('util')
|
||||
, EventEmitter = events.EventEmitter;
|
||||
|
||||
/**
|
||||
* Hixie Sender implementation
|
||||
*/
|
||||
|
||||
function Sender(socket) {
|
||||
if (this instanceof Sender === false) {
|
||||
throw new TypeError("Classes can't be function-called");
|
||||
}
|
||||
|
||||
events.EventEmitter.call(this);
|
||||
|
||||
this.socket = socket;
|
||||
this.continuationFrame = false;
|
||||
this.isClosed = false;
|
||||
}
|
||||
|
||||
module.exports = Sender;
|
||||
|
||||
/**
|
||||
* Inherits from EventEmitter.
|
||||
*/
|
||||
|
||||
util.inherits(Sender, events.EventEmitter);
|
||||
|
||||
/**
|
||||
* Frames and writes data.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Sender.prototype.send = function(data, options, cb) {
|
||||
if (this.isClosed) return;
|
||||
|
||||
var isString = typeof data == 'string'
|
||||
, length = isString ? Buffer.byteLength(data) : data.length
|
||||
, lengthbytes = (length > 127) ? 2 : 1 // assume less than 2**14 bytes
|
||||
, writeStartMarker = this.continuationFrame == false
|
||||
, writeEndMarker = !options || !(typeof options.fin != 'undefined' && !options.fin)
|
||||
, buffer = new Buffer((writeStartMarker ? ((options && options.binary) ? (1 + lengthbytes) : 1) : 0) + length + ((writeEndMarker && !(options && options.binary)) ? 1 : 0))
|
||||
, offset = writeStartMarker ? 1 : 0;
|
||||
|
||||
if (writeStartMarker) {
|
||||
if (options && options.binary) {
|
||||
buffer.write('\x80', 'binary');
|
||||
// assume length less than 2**14 bytes
|
||||
if (lengthbytes > 1)
|
||||
buffer.write(String.fromCharCode(128+length/128), offset++, 'binary');
|
||||
buffer.write(String.fromCharCode(length&0x7f), offset++, 'binary');
|
||||
} else
|
||||
buffer.write('\x00', 'binary');
|
||||
}
|
||||
|
||||
if (isString) buffer.write(data, offset, 'utf8');
|
||||
else data.copy(buffer, offset, 0);
|
||||
|
||||
if (writeEndMarker) {
|
||||
if (options && options.binary) {
|
||||
// sending binary, not writing end marker
|
||||
} else
|
||||
buffer.write('\xff', offset + length, 'binary');
|
||||
this.continuationFrame = false;
|
||||
}
|
||||
else this.continuationFrame = true;
|
||||
|
||||
try {
|
||||
this.socket.write(buffer, 'binary', cb);
|
||||
} catch (e) {
|
||||
this.error(e.toString());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a close instruction to the remote party.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Sender.prototype.close = function(code, data, mask, cb) {
|
||||
if (this.isClosed) return;
|
||||
this.isClosed = true;
|
||||
try {
|
||||
if (this.continuationFrame) this.socket.write(new Buffer([0xff], 'binary'));
|
||||
this.socket.write(new Buffer([0xff, 0x00]), 'binary', cb);
|
||||
} catch (e) {
|
||||
this.error(e.toString());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a ping message to the remote party. Not available for hixie.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Sender.prototype.ping = function(data, options) {};
|
||||
|
||||
/**
|
||||
* Sends a pong message to the remote party. Not available for hixie.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Sender.prototype.pong = function(data, options) {};
|
||||
|
||||
/**
|
||||
* Handles an error
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Sender.prototype.error = function (reason) {
|
||||
this.emit('error', reason);
|
||||
return this;
|
||||
};
|
|
@ -0,0 +1,324 @@
|
|||
/*!
|
||||
* ws: a node.js websocket client
|
||||
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
var events = require('events')
|
||||
, util = require('util')
|
||||
, EventEmitter = events.EventEmitter
|
||||
, ErrorCodes = require('./ErrorCodes')
|
||||
, bufferUtil = require('./BufferUtil').BufferUtil
|
||||
, PerMessageDeflate = require('./PerMessageDeflate');
|
||||
|
||||
/**
|
||||
* HyBi Sender implementation
|
||||
*/
|
||||
|
||||
function Sender(socket, extensions) {
|
||||
if (this instanceof Sender === false) {
|
||||
throw new TypeError("Classes can't be function-called");
|
||||
}
|
||||
|
||||
events.EventEmitter.call(this);
|
||||
|
||||
this._socket = socket;
|
||||
this.extensions = extensions || {};
|
||||
this.firstFragment = true;
|
||||
this.compress = false;
|
||||
this.messageHandlers = [];
|
||||
this.processing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inherits from EventEmitter.
|
||||
*/
|
||||
|
||||
util.inherits(Sender, events.EventEmitter);
|
||||
|
||||
/**
|
||||
* Sends a close instruction to the remote party.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Sender.prototype.close = function(code, data, mask, cb) {
|
||||
if (typeof code !== 'undefined') {
|
||||
if (typeof code !== 'number' ||
|
||||
!ErrorCodes.isValidErrorCode(code)) throw new Error('first argument must be a valid error code number');
|
||||
}
|
||||
code = code || 1000;
|
||||
var dataBuffer = new Buffer(2 + (data ? Buffer.byteLength(data) : 0));
|
||||
writeUInt16BE.call(dataBuffer, code, 0);
|
||||
if (dataBuffer.length > 2) dataBuffer.write(data, 2);
|
||||
|
||||
var self = this;
|
||||
this.messageHandlers.push(function(callback) {
|
||||
self.frameAndSend(0x8, dataBuffer, true, mask);
|
||||
callback();
|
||||
if (typeof cb == 'function') cb();
|
||||
});
|
||||
this.flush();
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a ping message to the remote party.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Sender.prototype.ping = function(data, options) {
|
||||
var mask = options && options.mask;
|
||||
var self = this;
|
||||
this.messageHandlers.push(function(callback) {
|
||||
self.frameAndSend(0x9, data || '', true, mask);
|
||||
callback();
|
||||
});
|
||||
this.flush();
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a pong message to the remote party.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Sender.prototype.pong = function(data, options) {
|
||||
var mask = options && options.mask;
|
||||
var self = this;
|
||||
this.messageHandlers.push(function(callback) {
|
||||
self.frameAndSend(0xa, data || '', true, mask);
|
||||
callback();
|
||||
});
|
||||
this.flush();
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends text or binary data to the remote party.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Sender.prototype.send = function(data, options, cb) {
|
||||
var finalFragment = options && options.fin === false ? false : true;
|
||||
var mask = options && options.mask;
|
||||
var compress = options && options.compress;
|
||||
var opcode = options && options.binary ? 2 : 1;
|
||||
if (this.firstFragment === false) {
|
||||
opcode = 0;
|
||||
compress = false;
|
||||
} else {
|
||||
this.firstFragment = false;
|
||||
this.compress = compress;
|
||||
}
|
||||
if (finalFragment) this.firstFragment = true
|
||||
|
||||
var compressFragment = this.compress;
|
||||
|
||||
var self = this;
|
||||
this.messageHandlers.push(function(callback) {
|
||||
self.applyExtensions(data, finalFragment, compressFragment, function(err, data) {
|
||||
if (err) {
|
||||
if (typeof cb == 'function') cb(err);
|
||||
else self.emit('error', err);
|
||||
return;
|
||||
}
|
||||
self.frameAndSend(opcode, data, finalFragment, mask, compress, cb);
|
||||
callback();
|
||||
});
|
||||
});
|
||||
this.flush();
|
||||
};
|
||||
|
||||
/**
|
||||
* Frames and sends a piece of data according to the HyBi WebSocket protocol.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Sender.prototype.frameAndSend = function(opcode, data, finalFragment, maskData, compressed, cb) {
|
||||
var canModifyData = false;
|
||||
|
||||
if (!data) {
|
||||
try {
|
||||
this._socket.write(new Buffer([opcode | (finalFragment ? 0x80 : 0), 0 | (maskData ? 0x80 : 0)].concat(maskData ? [0, 0, 0, 0] : [])), 'binary', cb);
|
||||
}
|
||||
catch (e) {
|
||||
if (typeof cb == 'function') cb(e);
|
||||
else this.emit('error', e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Buffer.isBuffer(data)) {
|
||||
canModifyData = true;
|
||||
if (data && (typeof data.byteLength !== 'undefined' || typeof data.buffer !== 'undefined')) {
|
||||
data = getArrayBuffer(data);
|
||||
} else {
|
||||
//
|
||||
// If people want to send a number, this would allocate the number in
|
||||
// bytes as memory size instead of storing the number as buffer value. So
|
||||
// we need to transform it to string in order to prevent possible
|
||||
// vulnerabilities / memory attacks.
|
||||
//
|
||||
if (typeof data === 'number') data = data.toString();
|
||||
|
||||
data = new Buffer(data);
|
||||
}
|
||||
}
|
||||
|
||||
var dataLength = data.length
|
||||
, dataOffset = maskData ? 6 : 2
|
||||
, secondByte = dataLength;
|
||||
|
||||
if (dataLength >= 65536) {
|
||||
dataOffset += 8;
|
||||
secondByte = 127;
|
||||
}
|
||||
else if (dataLength > 125) {
|
||||
dataOffset += 2;
|
||||
secondByte = 126;
|
||||
}
|
||||
|
||||
var mergeBuffers = dataLength < 32768 || (maskData && !canModifyData);
|
||||
var totalLength = mergeBuffers ? dataLength + dataOffset : dataOffset;
|
||||
var outputBuffer = new Buffer(totalLength);
|
||||
outputBuffer[0] = finalFragment ? opcode | 0x80 : opcode;
|
||||
if (compressed) outputBuffer[0] |= 0x40;
|
||||
|
||||
switch (secondByte) {
|
||||
case 126:
|
||||
writeUInt16BE.call(outputBuffer, dataLength, 2);
|
||||
break;
|
||||
case 127:
|
||||
writeUInt32BE.call(outputBuffer, 0, 2);
|
||||
writeUInt32BE.call(outputBuffer, dataLength, 6);
|
||||
}
|
||||
|
||||
if (maskData) {
|
||||
outputBuffer[1] = secondByte | 0x80;
|
||||
var mask = this._randomMask || (this._randomMask = getRandomMask());
|
||||
outputBuffer[dataOffset - 4] = mask[0];
|
||||
outputBuffer[dataOffset - 3] = mask[1];
|
||||
outputBuffer[dataOffset - 2] = mask[2];
|
||||
outputBuffer[dataOffset - 1] = mask[3];
|
||||
if (mergeBuffers) {
|
||||
bufferUtil.mask(data, mask, outputBuffer, dataOffset, dataLength);
|
||||
try {
|
||||
this._socket.write(outputBuffer, 'binary', cb);
|
||||
}
|
||||
catch (e) {
|
||||
if (typeof cb == 'function') cb(e);
|
||||
else this.emit('error', e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
bufferUtil.mask(data, mask, data, 0, dataLength);
|
||||
try {
|
||||
this._socket.write(outputBuffer, 'binary');
|
||||
this._socket.write(data, 'binary', cb);
|
||||
}
|
||||
catch (e) {
|
||||
if (typeof cb == 'function') cb(e);
|
||||
else this.emit('error', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
outputBuffer[1] = secondByte;
|
||||
if (mergeBuffers) {
|
||||
data.copy(outputBuffer, dataOffset);
|
||||
try {
|
||||
this._socket.write(outputBuffer, 'binary', cb);
|
||||
}
|
||||
catch (e) {
|
||||
if (typeof cb == 'function') cb(e);
|
||||
else this.emit('error', e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
try {
|
||||
this._socket.write(outputBuffer, 'binary');
|
||||
this._socket.write(data, 'binary', cb);
|
||||
}
|
||||
catch (e) {
|
||||
if (typeof cb == 'function') cb(e);
|
||||
else this.emit('error', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute message handler buffers
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Sender.prototype.flush = function() {
|
||||
if (this.processing) return;
|
||||
|
||||
var handler = this.messageHandlers.shift();
|
||||
if (!handler) return;
|
||||
|
||||
this.processing = true;
|
||||
|
||||
var self = this;
|
||||
|
||||
handler(function() {
|
||||
self.processing = false;
|
||||
self.flush();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply extensions to message
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Sender.prototype.applyExtensions = function(data, fin, compress, callback) {
|
||||
if (compress && data) {
|
||||
if ((data.buffer || data) instanceof ArrayBuffer) {
|
||||
data = getArrayBuffer(data);
|
||||
}
|
||||
this.extensions[PerMessageDeflate.extensionName].compress(data, fin, callback);
|
||||
} else {
|
||||
callback(null, data);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Sender;
|
||||
|
||||
function writeUInt16BE(value, offset) {
|
||||
this[offset] = (value & 0xff00)>>8;
|
||||
this[offset+1] = value & 0xff;
|
||||
}
|
||||
|
||||
function writeUInt32BE(value, offset) {
|
||||
this[offset] = (value & 0xff000000)>>24;
|
||||
this[offset+1] = (value & 0xff0000)>>16;
|
||||
this[offset+2] = (value & 0xff00)>>8;
|
||||
this[offset+3] = value & 0xff;
|
||||
}
|
||||
|
||||
function getArrayBuffer(data) {
|
||||
// data is either an ArrayBuffer or ArrayBufferView.
|
||||
var array = new Uint8Array(data.buffer || data)
|
||||
, l = data.byteLength || data.length
|
||||
, o = data.byteOffset || 0
|
||||
, buffer = new Buffer(l);
|
||||
for (var i = 0; i < l; ++i) {
|
||||
buffer[i] = array[o+i];
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function getRandomMask() {
|
||||
return new Buffer([
|
||||
~~(Math.random() * 255),
|
||||
~~(Math.random() * 255),
|
||||
~~(Math.random() * 255),
|
||||
~~(Math.random() * 255)
|
||||
]);
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
/*!
|
||||
* ws: a node.js websocket client
|
||||
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
module.exports.Validation = {
|
||||
isValidUTF8: function(buffer) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
'use strict';
|
||||
|
||||
/*!
|
||||
* ws: a node.js websocket client
|
||||
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
try {
|
||||
module.exports = require('utf-8-validate');
|
||||
} catch (e) {
|
||||
module.exports = require('./Validation.fallback');
|
||||
}
|
|
@ -0,0 +1,965 @@
|
|||
'use strict';
|
||||
|
||||
/*!
|
||||
* ws: a node.js websocket client
|
||||
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
var url = require('url')
|
||||
, util = require('util')
|
||||
, http = require('http')
|
||||
, https = require('https')
|
||||
, crypto = require('crypto')
|
||||
, stream = require('stream')
|
||||
, Ultron = require('ultron')
|
||||
, Options = require('options')
|
||||
, Sender = require('./Sender')
|
||||
, Receiver = require('./Receiver')
|
||||
, SenderHixie = require('./Sender.hixie')
|
||||
, ReceiverHixie = require('./Receiver.hixie')
|
||||
, Extensions = require('./Extensions')
|
||||
, PerMessageDeflate = require('./PerMessageDeflate')
|
||||
, EventEmitter = require('events').EventEmitter;
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
// Default protocol version
|
||||
|
||||
var protocolVersion = 13;
|
||||
|
||||
// Close timeout
|
||||
|
||||
var closeTimeout = 30 * 1000; // Allow 30 seconds to terminate the connection cleanly
|
||||
|
||||
/**
|
||||
* WebSocket implementation
|
||||
*
|
||||
* @constructor
|
||||
* @param {String} address Connection address.
|
||||
* @param {String|Array} protocols WebSocket protocols.
|
||||
* @param {Object} options Additional connection options.
|
||||
* @api public
|
||||
*/
|
||||
function WebSocket(address, protocols, options) {
|
||||
if (this instanceof WebSocket === false) {
|
||||
return new WebSocket(address, protocols, options);
|
||||
}
|
||||
|
||||
EventEmitter.call(this);
|
||||
|
||||
if (protocols && !Array.isArray(protocols) && 'object' === typeof protocols) {
|
||||
// accept the "options" Object as the 2nd argument
|
||||
options = protocols;
|
||||
protocols = null;
|
||||
}
|
||||
|
||||
if ('string' === typeof protocols) {
|
||||
protocols = [ protocols ];
|
||||
}
|
||||
|
||||
if (!Array.isArray(protocols)) {
|
||||
protocols = [];
|
||||
}
|
||||
|
||||
this._socket = null;
|
||||
this._ultron = null;
|
||||
this._closeReceived = false;
|
||||
this.bytesReceived = 0;
|
||||
this.readyState = null;
|
||||
this.supports = {};
|
||||
this.extensions = {};
|
||||
|
||||
if (Array.isArray(address)) {
|
||||
initAsServerClient.apply(this, address.concat(options));
|
||||
} else {
|
||||
initAsClient.apply(this, [address, protocols, options]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inherits from EventEmitter.
|
||||
*/
|
||||
util.inherits(WebSocket, EventEmitter);
|
||||
|
||||
/**
|
||||
* Ready States
|
||||
*/
|
||||
["CONNECTING", "OPEN", "CLOSING", "CLOSED"].forEach(function each(state, index) {
|
||||
WebSocket.prototype[state] = WebSocket[state] = index;
|
||||
});
|
||||
|
||||
/**
|
||||
* Gracefully closes the connection, after sending a description message to the server
|
||||
*
|
||||
* @param {Object} data to be sent to the server
|
||||
* @api public
|
||||
*/
|
||||
WebSocket.prototype.close = function close(code, data) {
|
||||
if (this.readyState === WebSocket.CLOSED) return;
|
||||
|
||||
if (this.readyState === WebSocket.CONNECTING) {
|
||||
this.readyState = WebSocket.CLOSED;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.readyState === WebSocket.CLOSING) {
|
||||
if (this._closeReceived && this._isServer) {
|
||||
this.terminate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
try {
|
||||
this.readyState = WebSocket.CLOSING;
|
||||
this._closeCode = code;
|
||||
this._closeMessage = data;
|
||||
var mask = !this._isServer;
|
||||
this._sender.close(code, data, mask, function(err) {
|
||||
if (err) self.emit('error', err);
|
||||
|
||||
if (self._closeReceived && self._isServer) {
|
||||
self.terminate();
|
||||
} else {
|
||||
// ensure that the connection is cleaned up even when no response of closing handshake.
|
||||
clearTimeout(self._closeTimer);
|
||||
self._closeTimer = setTimeout(cleanupWebsocketResources.bind(self, true), closeTimeout);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
this.emit('error', e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Pause the client stream
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
WebSocket.prototype.pause = function pauser() {
|
||||
if (this.readyState !== WebSocket.OPEN) throw new Error('not opened');
|
||||
|
||||
return this._socket.pause();
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a ping
|
||||
*
|
||||
* @param {Object} data to be sent to the server
|
||||
* @param {Object} Members - mask: boolean, binary: boolean
|
||||
* @param {boolean} dontFailWhenClosed indicates whether or not to throw if the connection isnt open
|
||||
* @api public
|
||||
*/
|
||||
WebSocket.prototype.ping = function ping(data, options, dontFailWhenClosed) {
|
||||
if (this.readyState !== WebSocket.OPEN) {
|
||||
if (dontFailWhenClosed === true) return;
|
||||
throw new Error('not opened');
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
if (typeof options.mask === 'undefined') options.mask = !this._isServer;
|
||||
|
||||
this._sender.ping(data, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a pong
|
||||
*
|
||||
* @param {Object} data to be sent to the server
|
||||
* @param {Object} Members - mask: boolean, binary: boolean
|
||||
* @param {boolean} dontFailWhenClosed indicates whether or not to throw if the connection isnt open
|
||||
* @api public
|
||||
*/
|
||||
WebSocket.prototype.pong = function(data, options, dontFailWhenClosed) {
|
||||
if (this.readyState !== WebSocket.OPEN) {
|
||||
if (dontFailWhenClosed === true) return;
|
||||
throw new Error('not opened');
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
if (typeof options.mask === 'undefined') options.mask = !this._isServer;
|
||||
|
||||
this._sender.pong(data, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Resume the client stream
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
WebSocket.prototype.resume = function resume() {
|
||||
if (this.readyState !== WebSocket.OPEN) throw new Error('not opened');
|
||||
|
||||
return this._socket.resume();
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a piece of data
|
||||
*
|
||||
* @param {Object} data to be sent to the server
|
||||
* @param {Object} Members - mask: boolean, binary: boolean, compress: boolean
|
||||
* @param {function} Optional callback which is executed after the send completes
|
||||
* @api public
|
||||
*/
|
||||
|
||||
WebSocket.prototype.send = function send(data, options, cb) {
|
||||
if (typeof options === 'function') {
|
||||
cb = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
if (this.readyState !== WebSocket.OPEN) {
|
||||
if (typeof cb === 'function') cb(new Error('not opened'));
|
||||
else throw new Error('not opened');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data) data = '';
|
||||
if (this._queue) {
|
||||
var self = this;
|
||||
this._queue.push(function() { self.send(data, options, cb); });
|
||||
return;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
options.fin = true;
|
||||
|
||||
if (typeof options.binary === 'undefined') {
|
||||
options.binary = (data instanceof ArrayBuffer || data instanceof Buffer ||
|
||||
data instanceof Uint8Array ||
|
||||
data instanceof Uint16Array ||
|
||||
data instanceof Uint32Array ||
|
||||
data instanceof Int8Array ||
|
||||
data instanceof Int16Array ||
|
||||
data instanceof Int32Array ||
|
||||
data instanceof Float32Array ||
|
||||
data instanceof Float64Array);
|
||||
}
|
||||
|
||||
if (typeof options.mask === 'undefined') options.mask = !this._isServer;
|
||||
if (typeof options.compress === 'undefined') options.compress = true;
|
||||
if (!this.extensions[PerMessageDeflate.extensionName]) {
|
||||
options.compress = false;
|
||||
}
|
||||
|
||||
var readable = typeof stream.Readable === 'function'
|
||||
? stream.Readable
|
||||
: stream.Stream;
|
||||
|
||||
if (data instanceof readable) {
|
||||
startQueue(this);
|
||||
var self = this;
|
||||
|
||||
sendStream(this, data, options, function send(error) {
|
||||
process.nextTick(function tock() {
|
||||
executeQueueSends(self);
|
||||
});
|
||||
|
||||
if (typeof cb === 'function') cb(error);
|
||||
});
|
||||
} else {
|
||||
this._sender.send(data, options, cb);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Streams data through calls to a user supplied function
|
||||
*
|
||||
* @param {Object} Members - mask: boolean, binary: boolean, compress: boolean
|
||||
* @param {function} 'function (error, send)' which is executed on successive ticks of which send is 'function (data, final)'.
|
||||
* @api public
|
||||
*/
|
||||
WebSocket.prototype.stream = function stream(options, cb) {
|
||||
if (typeof options === 'function') {
|
||||
cb = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
if (typeof cb !== 'function') throw new Error('callback must be provided');
|
||||
|
||||
if (this.readyState !== WebSocket.OPEN) {
|
||||
if (typeof cb === 'function') cb(new Error('not opened'));
|
||||
else throw new Error('not opened');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._queue) {
|
||||
this._queue.push(function () { self.stream(options, cb); });
|
||||
return;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
if (typeof options.mask === 'undefined') options.mask = !this._isServer;
|
||||
if (typeof options.compress === 'undefined') options.compress = true;
|
||||
if (!this.extensions[PerMessageDeflate.extensionName]) {
|
||||
options.compress = false;
|
||||
}
|
||||
|
||||
startQueue(this);
|
||||
|
||||
function send(data, final) {
|
||||
try {
|
||||
if (self.readyState !== WebSocket.OPEN) throw new Error('not opened');
|
||||
options.fin = final === true;
|
||||
self._sender.send(data, options);
|
||||
if (!final) process.nextTick(cb.bind(null, null, send));
|
||||
else executeQueueSends(self);
|
||||
} catch (e) {
|
||||
if (typeof cb === 'function') cb(e);
|
||||
else {
|
||||
delete self._queue;
|
||||
self.emit('error', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.nextTick(cb.bind(null, null, send));
|
||||
};
|
||||
|
||||
/**
|
||||
* Immediately shuts down the connection
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
WebSocket.prototype.terminate = function terminate() {
|
||||
if (this.readyState === WebSocket.CLOSED) return;
|
||||
|
||||
if (this._socket) {
|
||||
this.readyState = WebSocket.CLOSING;
|
||||
|
||||
// End the connection
|
||||
try { this._socket.end(); }
|
||||
catch (e) {
|
||||
// Socket error during end() call, so just destroy it right now
|
||||
cleanupWebsocketResources.call(this, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a timeout to ensure that the connection is completely
|
||||
// cleaned up within 30 seconds, even if the clean close procedure
|
||||
// fails for whatever reason
|
||||
// First cleanup any pre-existing timeout from an earlier "terminate" call,
|
||||
// if one exists. Otherwise terminate calls in quick succession will leak timeouts
|
||||
// and hold the program open for `closeTimout` time.
|
||||
if (this._closeTimer) { clearTimeout(this._closeTimer); }
|
||||
this._closeTimer = setTimeout(cleanupWebsocketResources.bind(this, true), closeTimeout);
|
||||
} else if (this.readyState === WebSocket.CONNECTING) {
|
||||
cleanupWebsocketResources.call(this, true);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Expose bufferedAmount
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
Object.defineProperty(WebSocket.prototype, 'bufferedAmount', {
|
||||
get: function get() {
|
||||
var amount = 0;
|
||||
if (this._socket) {
|
||||
amount = this._socket.bufferSize || 0;
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Emulates the W3C Browser based WebSocket interface using function members.
|
||||
*
|
||||
* @see http://dev.w3.org/html5/websockets/#the-websocket-interface
|
||||
* @api public
|
||||
*/
|
||||
['open', 'error', 'close', 'message'].forEach(function(method) {
|
||||
Object.defineProperty(WebSocket.prototype, 'on' + method, {
|
||||
/**
|
||||
* Returns the current listener
|
||||
*
|
||||
* @returns {Mixed} the set function or undefined
|
||||
* @api public
|
||||
*/
|
||||
get: function get() {
|
||||
var listener = this.listeners(method)[0];
|
||||
return listener ? (listener._listener ? listener._listener : listener) : undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Start listening for events
|
||||
*
|
||||
* @param {Function} listener the listener
|
||||
* @returns {Mixed} the set function or undefined
|
||||
* @api public
|
||||
*/
|
||||
set: function set(listener) {
|
||||
this.removeAllListeners(method);
|
||||
this.addEventListener(method, listener);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Emulates the W3C Browser based WebSocket interface using addEventListener.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en/DOM/element.addEventListener
|
||||
* @see http://dev.w3.org/html5/websockets/#the-websocket-interface
|
||||
* @api public
|
||||
*/
|
||||
WebSocket.prototype.addEventListener = function(method, listener) {
|
||||
var target = this;
|
||||
|
||||
function onMessage (data, flags) {
|
||||
listener.call(target, new MessageEvent(data, !!flags.binary, target));
|
||||
}
|
||||
|
||||
function onClose (code, message) {
|
||||
listener.call(target, new CloseEvent(code, message, target));
|
||||
}
|
||||
|
||||
function onError (event) {
|
||||
event.type = 'error';
|
||||
event.target = target;
|
||||
listener.call(target, event);
|
||||
}
|
||||
|
||||
function onOpen () {
|
||||
listener.call(target, new OpenEvent(target));
|
||||
}
|
||||
|
||||
if (typeof listener === 'function') {
|
||||
if (method === 'message') {
|
||||
// store a reference so we can return the original function from the
|
||||
// addEventListener hook
|
||||
onMessage._listener = listener;
|
||||
this.on(method, onMessage);
|
||||
} else if (method === 'close') {
|
||||
// store a reference so we can return the original function from the
|
||||
// addEventListener hook
|
||||
onClose._listener = listener;
|
||||
this.on(method, onClose);
|
||||
} else if (method === 'error') {
|
||||
// store a reference so we can return the original function from the
|
||||
// addEventListener hook
|
||||
onError._listener = listener;
|
||||
this.on(method, onError);
|
||||
} else if (method === 'open') {
|
||||
// store a reference so we can return the original function from the
|
||||
// addEventListener hook
|
||||
onOpen._listener = listener;
|
||||
this.on(method, onOpen);
|
||||
} else {
|
||||
this.on(method, listener);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = WebSocket;
|
||||
module.exports.buildHostHeader = buildHostHeader
|
||||
|
||||
/**
|
||||
* W3C MessageEvent
|
||||
*
|
||||
* @see http://www.w3.org/TR/html5/comms.html
|
||||
* @constructor
|
||||
* @api private
|
||||
*/
|
||||
function MessageEvent(dataArg, isBinary, target) {
|
||||
this.type = 'message';
|
||||
this.data = dataArg;
|
||||
this.target = target;
|
||||
this.binary = isBinary; // non-standard.
|
||||
}
|
||||
|
||||
/**
|
||||
* W3C CloseEvent
|
||||
*
|
||||
* @see http://www.w3.org/TR/html5/comms.html
|
||||
* @constructor
|
||||
* @api private
|
||||
*/
|
||||
function CloseEvent(code, reason, target) {
|
||||
this.type = 'close';
|
||||
this.wasClean = (typeof code === 'undefined' || code === 1000);
|
||||
this.code = code;
|
||||
this.reason = reason;
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
/**
|
||||
* W3C OpenEvent
|
||||
*
|
||||
* @see http://www.w3.org/TR/html5/comms.html
|
||||
* @constructor
|
||||
* @api private
|
||||
*/
|
||||
function OpenEvent(target) {
|
||||
this.type = 'open';
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
// Append port number to Host header, only if specified in the url
|
||||
// and non-default
|
||||
function buildHostHeader(isSecure, hostname, port) {
|
||||
var headerHost = hostname;
|
||||
if (hostname) {
|
||||
if ((isSecure && (port != 443)) || (!isSecure && (port != 80))){
|
||||
headerHost = headerHost + ':' + port;
|
||||
}
|
||||
}
|
||||
return headerHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entirely private apis,
|
||||
* which may or may not be bound to a sepcific WebSocket instance.
|
||||
*/
|
||||
function initAsServerClient(req, socket, upgradeHead, options) {
|
||||
options = new Options({
|
||||
protocolVersion: protocolVersion,
|
||||
protocol: null,
|
||||
extensions: {}
|
||||
}).merge(options);
|
||||
|
||||
// expose state properties
|
||||
this.protocol = options.value.protocol;
|
||||
this.protocolVersion = options.value.protocolVersion;
|
||||
this.extensions = options.value.extensions;
|
||||
this.supports.binary = (this.protocolVersion !== 'hixie-76');
|
||||
this.upgradeReq = req;
|
||||
this.readyState = WebSocket.CONNECTING;
|
||||
this._isServer = true;
|
||||
|
||||
// establish connection
|
||||
if (options.value.protocolVersion === 'hixie-76') {
|
||||
establishConnection.call(this, ReceiverHixie, SenderHixie, socket, upgradeHead);
|
||||
} else {
|
||||
establishConnection.call(this, Receiver, Sender, socket, upgradeHead);
|
||||
}
|
||||
}
|
||||
|
||||
function initAsClient(address, protocols, options) {
|
||||
options = new Options({
|
||||
origin: null,
|
||||
protocolVersion: protocolVersion,
|
||||
host: null,
|
||||
headers: null,
|
||||
protocol: protocols.join(','),
|
||||
agent: null,
|
||||
|
||||
// ssl-related options
|
||||
pfx: null,
|
||||
key: null,
|
||||
passphrase: null,
|
||||
cert: null,
|
||||
ca: null,
|
||||
ciphers: null,
|
||||
rejectUnauthorized: null,
|
||||
perMessageDeflate: true,
|
||||
localAddress: null
|
||||
}).merge(options);
|
||||
|
||||
if (options.value.protocolVersion !== 8 && options.value.protocolVersion !== 13) {
|
||||
throw new Error('unsupported protocol version');
|
||||
}
|
||||
|
||||
// verify URL and establish http class
|
||||
var serverUrl = url.parse(address);
|
||||
var isUnixSocket = serverUrl.protocol === 'ws+unix:';
|
||||
if (!serverUrl.host && !isUnixSocket) throw new Error('invalid url');
|
||||
var isSecure = serverUrl.protocol === 'wss:' || serverUrl.protocol === 'https:';
|
||||
var httpObj = isSecure ? https : http;
|
||||
var port = serverUrl.port || (isSecure ? 443 : 80);
|
||||
var auth = serverUrl.auth;
|
||||
|
||||
// prepare extensions
|
||||
var extensionsOffer = {};
|
||||
var perMessageDeflate;
|
||||
if (options.value.perMessageDeflate) {
|
||||
perMessageDeflate = new PerMessageDeflate(typeof options.value.perMessageDeflate !== true ? options.value.perMessageDeflate : {}, false);
|
||||
extensionsOffer[PerMessageDeflate.extensionName] = perMessageDeflate.offer();
|
||||
}
|
||||
|
||||
// expose state properties
|
||||
this._isServer = false;
|
||||
this.url = address;
|
||||
this.protocolVersion = options.value.protocolVersion;
|
||||
this.supports.binary = (this.protocolVersion !== 'hixie-76');
|
||||
|
||||
// begin handshake
|
||||
var key = new Buffer(options.value.protocolVersion + '-' + Date.now()).toString('base64');
|
||||
var shasum = crypto.createHash('sha1');
|
||||
shasum.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
|
||||
var expectedServerKey = shasum.digest('base64');
|
||||
|
||||
var agent = options.value.agent;
|
||||
|
||||
var headerHost = buildHostHeader(isSecure, serverUrl.hostname, port)
|
||||
|
||||
var requestOptions = {
|
||||
port: port,
|
||||
host: serverUrl.hostname,
|
||||
headers: {
|
||||
'Connection': 'Upgrade',
|
||||
'Upgrade': 'websocket',
|
||||
'Host': headerHost,
|
||||
'Sec-WebSocket-Version': options.value.protocolVersion,
|
||||
'Sec-WebSocket-Key': key
|
||||
}
|
||||
};
|
||||
|
||||
// If we have basic auth.
|
||||
if (auth) {
|
||||
requestOptions.headers.Authorization = 'Basic ' + new Buffer(auth).toString('base64');
|
||||
}
|
||||
|
||||
if (options.value.protocol) {
|
||||
requestOptions.headers['Sec-WebSocket-Protocol'] = options.value.protocol;
|
||||
}
|
||||
|
||||
if (options.value.host) {
|
||||
requestOptions.headers.Host = options.value.host;
|
||||
}
|
||||
|
||||
if (options.value.headers) {
|
||||
for (var header in options.value.headers) {
|
||||
if (options.value.headers.hasOwnProperty(header)) {
|
||||
requestOptions.headers[header] = options.value.headers[header];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(extensionsOffer).length) {
|
||||
requestOptions.headers['Sec-WebSocket-Extensions'] = Extensions.format(extensionsOffer);
|
||||
}
|
||||
|
||||
if (options.isDefinedAndNonNull('pfx')
|
||||
|| options.isDefinedAndNonNull('key')
|
||||
|| options.isDefinedAndNonNull('passphrase')
|
||||
|| options.isDefinedAndNonNull('cert')
|
||||
|| options.isDefinedAndNonNull('ca')
|
||||
|| options.isDefinedAndNonNull('ciphers')
|
||||
|| options.isDefinedAndNonNull('rejectUnauthorized')) {
|
||||
|
||||
if (options.isDefinedAndNonNull('pfx')) requestOptions.pfx = options.value.pfx;
|
||||
if (options.isDefinedAndNonNull('key')) requestOptions.key = options.value.key;
|
||||
if (options.isDefinedAndNonNull('passphrase')) requestOptions.passphrase = options.value.passphrase;
|
||||
if (options.isDefinedAndNonNull('cert')) requestOptions.cert = options.value.cert;
|
||||
if (options.isDefinedAndNonNull('ca')) requestOptions.ca = options.value.ca;
|
||||
if (options.isDefinedAndNonNull('ciphers')) requestOptions.ciphers = options.value.ciphers;
|
||||
if (options.isDefinedAndNonNull('rejectUnauthorized')) requestOptions.rejectUnauthorized = options.value.rejectUnauthorized;
|
||||
|
||||
if (!agent) {
|
||||
// global agent ignores client side certificates
|
||||
agent = new httpObj.Agent(requestOptions);
|
||||
}
|
||||
}
|
||||
|
||||
requestOptions.path = serverUrl.path || '/';
|
||||
|
||||
if (agent) {
|
||||
requestOptions.agent = agent;
|
||||
}
|
||||
|
||||
if (isUnixSocket) {
|
||||
requestOptions.socketPath = serverUrl.pathname;
|
||||
}
|
||||
|
||||
if (options.value.localAddress) {
|
||||
requestOptions.localAddress = options.value.localAddress;
|
||||
}
|
||||
|
||||
if (options.value.origin) {
|
||||
if (options.value.protocolVersion < 13) requestOptions.headers['Sec-WebSocket-Origin'] = options.value.origin;
|
||||
else requestOptions.headers.Origin = options.value.origin;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var req = httpObj.request(requestOptions);
|
||||
|
||||
req.on('error', function onerror(error) {
|
||||
self.emit('error', error);
|
||||
cleanupWebsocketResources.call(self, error);
|
||||
});
|
||||
|
||||
req.once('response', function response(res) {
|
||||
var error;
|
||||
|
||||
if (!self.emit('unexpected-response', req, res)) {
|
||||
error = new Error('unexpected server response (' + res.statusCode + ')');
|
||||
req.abort();
|
||||
self.emit('error', error);
|
||||
}
|
||||
|
||||
cleanupWebsocketResources.call(self, error);
|
||||
});
|
||||
|
||||
req.once('upgrade', function upgrade(res, socket, upgradeHead) {
|
||||
if (self.readyState === WebSocket.CLOSED) {
|
||||
// client closed before server accepted connection
|
||||
self.emit('close');
|
||||
self.removeAllListeners();
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
|
||||
var serverKey = res.headers['sec-websocket-accept'];
|
||||
if (typeof serverKey === 'undefined' || serverKey !== expectedServerKey) {
|
||||
self.emit('error', 'invalid server key');
|
||||
self.removeAllListeners();
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
|
||||
var serverProt = res.headers['sec-websocket-protocol'];
|
||||
var protList = (options.value.protocol || "").split(/, */);
|
||||
var protError = null;
|
||||
|
||||
if (!options.value.protocol && serverProt) {
|
||||
protError = 'server sent a subprotocol even though none requested';
|
||||
} else if (options.value.protocol && !serverProt) {
|
||||
protError = 'server sent no subprotocol even though requested';
|
||||
} else if (serverProt && protList.indexOf(serverProt) === -1) {
|
||||
protError = 'server responded with an invalid protocol';
|
||||
}
|
||||
|
||||
if (protError) {
|
||||
self.emit('error', protError);
|
||||
self.removeAllListeners();
|
||||
socket.end();
|
||||
return;
|
||||
} else if (serverProt) {
|
||||
self.protocol = serverProt;
|
||||
}
|
||||
|
||||
var serverExtensions = Extensions.parse(res.headers['sec-websocket-extensions']);
|
||||
if (perMessageDeflate && serverExtensions[PerMessageDeflate.extensionName]) {
|
||||
try {
|
||||
perMessageDeflate.accept(serverExtensions[PerMessageDeflate.extensionName]);
|
||||
} catch (err) {
|
||||
self.emit('error', 'invalid extension parameter');
|
||||
self.removeAllListeners();
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
self.extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
|
||||
}
|
||||
|
||||
establishConnection.call(self, Receiver, Sender, socket, upgradeHead);
|
||||
|
||||
// perform cleanup on http resources
|
||||
req.removeAllListeners();
|
||||
req = null;
|
||||
agent = null;
|
||||
});
|
||||
|
||||
req.end();
|
||||
this.readyState = WebSocket.CONNECTING;
|
||||
}
|
||||
|
||||
function establishConnection(ReceiverClass, SenderClass, socket, upgradeHead) {
|
||||
var ultron = this._ultron = new Ultron(socket)
|
||||
, called = false
|
||||
, self = this;
|
||||
|
||||
socket.setTimeout(0);
|
||||
socket.setNoDelay(true);
|
||||
|
||||
this._receiver = new ReceiverClass(this.extensions);
|
||||
this._socket = socket;
|
||||
|
||||
// socket cleanup handlers
|
||||
ultron.on('end', cleanupWebsocketResources.bind(this));
|
||||
ultron.on('close', cleanupWebsocketResources.bind(this));
|
||||
ultron.on('error', cleanupWebsocketResources.bind(this));
|
||||
|
||||
// ensure that the upgradeHead is added to the receiver
|
||||
function firstHandler(data) {
|
||||
if (called || self.readyState === WebSocket.CLOSED) return;
|
||||
|
||||
called = true;
|
||||
socket.removeListener('data', firstHandler);
|
||||
ultron.on('data', realHandler);
|
||||
|
||||
if (upgradeHead && upgradeHead.length > 0) {
|
||||
realHandler(upgradeHead);
|
||||
upgradeHead = null;
|
||||
}
|
||||
|
||||
if (data) realHandler(data);
|
||||
}
|
||||
|
||||
// subsequent packets are pushed straight to the receiver
|
||||
function realHandler(data) {
|
||||
self.bytesReceived += data.length;
|
||||
self._receiver.add(data);
|
||||
}
|
||||
|
||||
ultron.on('data', firstHandler);
|
||||
|
||||
// if data was passed along with the http upgrade,
|
||||
// this will schedule a push of that on to the receiver.
|
||||
// this has to be done on next tick, since the caller
|
||||
// hasn't had a chance to set event handlers on this client
|
||||
// object yet.
|
||||
process.nextTick(firstHandler);
|
||||
|
||||
// receiver event handlers
|
||||
self._receiver.ontext = function ontext(data, flags) {
|
||||
flags = flags || {};
|
||||
|
||||
self.emit('message', data, flags);
|
||||
};
|
||||
|
||||
self._receiver.onbinary = function onbinary(data, flags) {
|
||||
flags = flags || {};
|
||||
|
||||
flags.binary = true;
|
||||
self.emit('message', data, flags);
|
||||
};
|
||||
|
||||
self._receiver.onping = function onping(data, flags) {
|
||||
flags = flags || {};
|
||||
|
||||
self.pong(data, {
|
||||
mask: !self._isServer,
|
||||
binary: flags.binary === true
|
||||
}, true);
|
||||
|
||||
self.emit('ping', data, flags);
|
||||
};
|
||||
|
||||
self._receiver.onpong = function onpong(data, flags) {
|
||||
self.emit('pong', data, flags || {});
|
||||
};
|
||||
|
||||
self._receiver.onclose = function onclose(code, data, flags) {
|
||||
flags = flags || {};
|
||||
|
||||
self._closeReceived = true;
|
||||
self.close(code, data);
|
||||
};
|
||||
|
||||
self._receiver.onerror = function onerror(reason, errorCode) {
|
||||
// close the connection when the receiver reports a HyBi error code
|
||||
self.close(typeof errorCode !== 'undefined' ? errorCode : 1002, '');
|
||||
self.emit('error', reason, errorCode);
|
||||
};
|
||||
|
||||
// finalize the client
|
||||
this._sender = new SenderClass(socket, this.extensions);
|
||||
this._sender.on('error', function onerror(error) {
|
||||
self.close(1002, '');
|
||||
self.emit('error', error);
|
||||
});
|
||||
|
||||
this.readyState = WebSocket.OPEN;
|
||||
this.emit('open');
|
||||
}
|
||||
|
||||
function startQueue(instance) {
|
||||
instance._queue = instance._queue || [];
|
||||
}
|
||||
|
||||
function executeQueueSends(instance) {
|
||||
var queue = instance._queue;
|
||||
if (typeof queue === 'undefined') return;
|
||||
|
||||
delete instance._queue;
|
||||
for (var i = 0, l = queue.length; i < l; ++i) {
|
||||
queue[i]();
|
||||
}
|
||||
}
|
||||
|
||||
function sendStream(instance, stream, options, cb) {
|
||||
stream.on('data', function incoming(data) {
|
||||
if (instance.readyState !== WebSocket.OPEN) {
|
||||
if (typeof cb === 'function') cb(new Error('not opened'));
|
||||
else {
|
||||
delete instance._queue;
|
||||
instance.emit('error', new Error('not opened'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
options.fin = false;
|
||||
instance._sender.send(data, options);
|
||||
});
|
||||
|
||||
stream.on('end', function end() {
|
||||
if (instance.readyState !== WebSocket.OPEN) {
|
||||
if (typeof cb === 'function') cb(new Error('not opened'));
|
||||
else {
|
||||
delete instance._queue;
|
||||
instance.emit('error', new Error('not opened'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
options.fin = true;
|
||||
instance._sender.send(null, options);
|
||||
|
||||
if (typeof cb === 'function') cb(null);
|
||||
});
|
||||
}
|
||||
|
||||
function cleanupWebsocketResources(error) {
|
||||
if (this.readyState === WebSocket.CLOSED) return;
|
||||
|
||||
var emitClose = this.readyState !== WebSocket.CONNECTING;
|
||||
this.readyState = WebSocket.CLOSED;
|
||||
|
||||
clearTimeout(this._closeTimer);
|
||||
this._closeTimer = null;
|
||||
|
||||
if (emitClose) {
|
||||
// If the connection was closed abnormally (with an error), or if
|
||||
// the close control frame was not received then the close code
|
||||
// must default to 1006.
|
||||
if (error || !this._closeReceived) {
|
||||
this._closeCode = 1006;
|
||||
}
|
||||
this.emit('close', this._closeCode || 1000, this._closeMessage || '');
|
||||
}
|
||||
|
||||
if (this._socket) {
|
||||
if (this._ultron) this._ultron.destroy();
|
||||
this._socket.on('error', function onerror() {
|
||||
try { this.destroy(); }
|
||||
catch (e) {}
|
||||
});
|
||||
|
||||
try {
|
||||
if (!error) this._socket.end();
|
||||
else this._socket.destroy();
|
||||
} catch (e) { /* Ignore termination errors */ }
|
||||
|
||||
this._socket = null;
|
||||
this._ultron = null;
|
||||
}
|
||||
|
||||
if (this._sender) {
|
||||
this._sender.removeAllListeners();
|
||||
this._sender = null;
|
||||
}
|
||||
|
||||
if (this._receiver) {
|
||||
this._receiver.cleanup();
|
||||
this._receiver = null;
|
||||
}
|
||||
|
||||
if (this.extensions[PerMessageDeflate.extensionName]) {
|
||||
this.extensions[PerMessageDeflate.extensionName].cleanup();
|
||||
}
|
||||
|
||||
this.extensions = null;
|
||||
|
||||
this.removeAllListeners();
|
||||
this.on('error', function onerror() {}); // catch all errors after this
|
||||
delete this._queue;
|
||||
}
|
|
@ -0,0 +1,513 @@
|
|||
/*!
|
||||
* ws: a node.js websocket client
|
||||
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
var util = require('util')
|
||||
, events = require('events')
|
||||
, http = require('http')
|
||||
, crypto = require('crypto')
|
||||
, Options = require('options')
|
||||
, WebSocket = require('./WebSocket')
|
||||
, Extensions = require('./Extensions')
|
||||
, PerMessageDeflate = require('./PerMessageDeflate')
|
||||
, tls = require('tls')
|
||||
, url = require('url');
|
||||
|
||||
/**
|
||||
* WebSocket Server implementation
|
||||
*/
|
||||
|
||||
function WebSocketServer(options, callback) {
|
||||
if (this instanceof WebSocketServer === false) {
|
||||
return new WebSocketServer(options, callback);
|
||||
}
|
||||
|
||||
events.EventEmitter.call(this);
|
||||
|
||||
options = new Options({
|
||||
host: '0.0.0.0',
|
||||
port: null,
|
||||
server: null,
|
||||
verifyClient: null,
|
||||
handleProtocols: null,
|
||||
path: null,
|
||||
noServer: false,
|
||||
disableHixie: false,
|
||||
clientTracking: true,
|
||||
perMessageDeflate: true
|
||||
}).merge(options);
|
||||
|
||||
if (!options.isDefinedAndNonNull('port') && !options.isDefinedAndNonNull('server') && !options.value.noServer) {
|
||||
throw new TypeError('`port` or a `server` must be provided');
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
if (options.isDefinedAndNonNull('port')) {
|
||||
this._server = http.createServer(function (req, res) {
|
||||
var body = http.STATUS_CODES[426];
|
||||
res.writeHead(426, {
|
||||
'Content-Length': body.length,
|
||||
'Content-Type': 'text/plain'
|
||||
});
|
||||
res.end(body);
|
||||
});
|
||||
this._server.allowHalfOpen = false;
|
||||
this._server.listen(options.value.port, options.value.host, callback);
|
||||
this._closeServer = function() { if (self._server) self._server.close(); };
|
||||
}
|
||||
else if (options.value.server) {
|
||||
this._server = options.value.server;
|
||||
if (options.value.path) {
|
||||
// take note of the path, to avoid collisions when multiple websocket servers are
|
||||
// listening on the same http server
|
||||
if (this._server._webSocketPaths && options.value.server._webSocketPaths[options.value.path]) {
|
||||
throw new Error('two instances of WebSocketServer cannot listen on the same http server path');
|
||||
}
|
||||
if (typeof this._server._webSocketPaths !== 'object') {
|
||||
this._server._webSocketPaths = {};
|
||||
}
|
||||
this._server._webSocketPaths[options.value.path] = 1;
|
||||
}
|
||||
}
|
||||
if (this._server) this._server.once('listening', function() { self.emit('listening'); });
|
||||
|
||||
if (typeof this._server != 'undefined') {
|
||||
this._server.on('error', function(error) {
|
||||
self.emit('error', error)
|
||||
});
|
||||
this._server.on('upgrade', function(req, socket, upgradeHead) {
|
||||
//copy upgradeHead to avoid retention of large slab buffers used in node core
|
||||
var head = new Buffer(upgradeHead.length);
|
||||
upgradeHead.copy(head);
|
||||
|
||||
self.handleUpgrade(req, socket, head, function(client) {
|
||||
self.emit('connection'+req.url, client);
|
||||
self.emit('connection', client);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.options = options.value;
|
||||
this.path = options.value.path;
|
||||
this.clients = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Inherits from EventEmitter.
|
||||
*/
|
||||
|
||||
util.inherits(WebSocketServer, events.EventEmitter);
|
||||
|
||||
/**
|
||||
* Immediately shuts down the connection.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
WebSocketServer.prototype.close = function(callback) {
|
||||
// terminate all associated clients
|
||||
var error = null;
|
||||
try {
|
||||
for (var i = 0, l = this.clients.length; i < l; ++i) {
|
||||
this.clients[i].terminate();
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
// remove path descriptor, if any
|
||||
if (this.path && this._server._webSocketPaths) {
|
||||
delete this._server._webSocketPaths[this.path];
|
||||
if (Object.keys(this._server._webSocketPaths).length == 0) {
|
||||
delete this._server._webSocketPaths;
|
||||
}
|
||||
}
|
||||
|
||||
// close the http server if it was internally created
|
||||
try {
|
||||
if (typeof this._closeServer !== 'undefined') {
|
||||
this._closeServer();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
delete this._server;
|
||||
}
|
||||
if(callback)
|
||||
callback(error);
|
||||
else if(error)
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a HTTP Upgrade request.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
WebSocketServer.prototype.handleUpgrade = function(req, socket, upgradeHead, cb) {
|
||||
// check for wrong path
|
||||
if (this.options.path) {
|
||||
var u = url.parse(req.url);
|
||||
if (u && u.pathname !== this.options.path) return;
|
||||
}
|
||||
|
||||
if (typeof req.headers.upgrade === 'undefined' || req.headers.upgrade.toLowerCase() !== 'websocket') {
|
||||
abortConnection(socket, 400, 'Bad Request');
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.headers['sec-websocket-key1']) handleHixieUpgrade.apply(this, arguments);
|
||||
else handleHybiUpgrade.apply(this, arguments);
|
||||
}
|
||||
|
||||
module.exports = WebSocketServer;
|
||||
|
||||
/**
|
||||
* Entirely private apis,
|
||||
* which may or may not be bound to a sepcific WebSocket instance.
|
||||
*/
|
||||
|
||||
function handleHybiUpgrade(req, socket, upgradeHead, cb) {
|
||||
// handle premature socket errors
|
||||
var errorHandler = function() {
|
||||
try { socket.destroy(); } catch (e) {}
|
||||
}
|
||||
socket.on('error', errorHandler);
|
||||
|
||||
// verify key presence
|
||||
if (!req.headers['sec-websocket-key']) {
|
||||
abortConnection(socket, 400, 'Bad Request');
|
||||
return;
|
||||
}
|
||||
|
||||
// verify version
|
||||
var version = parseInt(req.headers['sec-websocket-version']);
|
||||
if ([8, 13].indexOf(version) === -1) {
|
||||
abortConnection(socket, 400, 'Bad Request');
|
||||
return;
|
||||
}
|
||||
|
||||
// verify protocol
|
||||
var protocols = req.headers['sec-websocket-protocol'];
|
||||
|
||||
// verify client
|
||||
var origin = version < 13 ?
|
||||
req.headers['sec-websocket-origin'] :
|
||||
req.headers['origin'];
|
||||
|
||||
// handle extensions offer
|
||||
var extensionsOffer = Extensions.parse(req.headers['sec-websocket-extensions']);
|
||||
|
||||
// handler to call when the connection sequence completes
|
||||
var self = this;
|
||||
var completeHybiUpgrade2 = function(protocol) {
|
||||
|
||||
// calc key
|
||||
var key = req.headers['sec-websocket-key'];
|
||||
var shasum = crypto.createHash('sha1');
|
||||
shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
|
||||
key = shasum.digest('base64');
|
||||
|
||||
var headers = [
|
||||
'HTTP/1.1 101 Switching Protocols'
|
||||
, 'Upgrade: websocket'
|
||||
, 'Connection: Upgrade'
|
||||
, 'Sec-WebSocket-Accept: ' + key
|
||||
];
|
||||
|
||||
if (typeof protocol != 'undefined') {
|
||||
headers.push('Sec-WebSocket-Protocol: ' + protocol);
|
||||
}
|
||||
|
||||
var extensions = {};
|
||||
try {
|
||||
extensions = acceptExtensions.call(self, extensionsOffer);
|
||||
} catch (err) {
|
||||
abortConnection(socket, 400, 'Bad Request');
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(extensions).length) {
|
||||
var serverExtensions = {};
|
||||
Object.keys(extensions).forEach(function(token) {
|
||||
serverExtensions[token] = [extensions[token].params]
|
||||
});
|
||||
headers.push('Sec-WebSocket-Extensions: ' + Extensions.format(serverExtensions));
|
||||
}
|
||||
|
||||
// allows external modification/inspection of handshake headers
|
||||
self.emit('headers', headers);
|
||||
|
||||
socket.setTimeout(0);
|
||||
socket.setNoDelay(true);
|
||||
try {
|
||||
socket.write(headers.concat('', '').join('\r\n'));
|
||||
}
|
||||
catch (e) {
|
||||
// if the upgrade write fails, shut the connection down hard
|
||||
try { socket.destroy(); } catch (e) {}
|
||||
return;
|
||||
}
|
||||
|
||||
var client = new WebSocket([req, socket, upgradeHead], {
|
||||
protocolVersion: version,
|
||||
protocol: protocol,
|
||||
extensions: extensions
|
||||
});
|
||||
|
||||
if (self.options.clientTracking) {
|
||||
self.clients.push(client);
|
||||
client.on('close', function() {
|
||||
var index = self.clients.indexOf(client);
|
||||
if (index != -1) {
|
||||
self.clients.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// signal upgrade complete
|
||||
socket.removeListener('error', errorHandler);
|
||||
cb(client);
|
||||
}
|
||||
|
||||
// optionally call external protocol selection handler before
|
||||
// calling completeHybiUpgrade2
|
||||
var completeHybiUpgrade1 = function() {
|
||||
// choose from the sub-protocols
|
||||
if (typeof self.options.handleProtocols == 'function') {
|
||||
var protList = (protocols || "").split(/, */);
|
||||
var callbackCalled = false;
|
||||
var res = self.options.handleProtocols(protList, function(result, protocol) {
|
||||
callbackCalled = true;
|
||||
if (!result) abortConnection(socket, 401, 'Unauthorized');
|
||||
else completeHybiUpgrade2(protocol);
|
||||
});
|
||||
if (!callbackCalled) {
|
||||
// the handleProtocols handler never called our callback
|
||||
abortConnection(socket, 501, 'Could not process protocols');
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
if (typeof protocols !== 'undefined') {
|
||||
completeHybiUpgrade2(protocols.split(/, */)[0]);
|
||||
}
|
||||
else {
|
||||
completeHybiUpgrade2();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// optionally call external client verification handler
|
||||
if (typeof this.options.verifyClient == 'function') {
|
||||
var info = {
|
||||
origin: origin,
|
||||
secure: typeof req.connection.authorized !== 'undefined' || typeof req.connection.encrypted !== 'undefined',
|
||||
req: req
|
||||
};
|
||||
if (this.options.verifyClient.length == 2) {
|
||||
this.options.verifyClient(info, function(result, code, name) {
|
||||
if (typeof code === 'undefined') code = 401;
|
||||
if (typeof name === 'undefined') name = http.STATUS_CODES[code];
|
||||
|
||||
if (!result) abortConnection(socket, code, name);
|
||||
else completeHybiUpgrade1();
|
||||
});
|
||||
return;
|
||||
}
|
||||
else if (!this.options.verifyClient(info)) {
|
||||
abortConnection(socket, 401, 'Unauthorized');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
completeHybiUpgrade1();
|
||||
}
|
||||
|
||||
function handleHixieUpgrade(req, socket, upgradeHead, cb) {
|
||||
// handle premature socket errors
|
||||
var errorHandler = function() {
|
||||
try { socket.destroy(); } catch (e) {}
|
||||
}
|
||||
socket.on('error', errorHandler);
|
||||
|
||||
// bail if options prevent hixie
|
||||
if (this.options.disableHixie) {
|
||||
abortConnection(socket, 401, 'Hixie support disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
// verify key presence
|
||||
if (!req.headers['sec-websocket-key2']) {
|
||||
abortConnection(socket, 400, 'Bad Request');
|
||||
return;
|
||||
}
|
||||
|
||||
var origin = req.headers['origin']
|
||||
, self = this;
|
||||
|
||||
// setup handshake completion to run after client has been verified
|
||||
var onClientVerified = function() {
|
||||
var wshost;
|
||||
if (!req.headers['x-forwarded-host'])
|
||||
wshost = req.headers.host;
|
||||
else
|
||||
wshost = req.headers['x-forwarded-host'];
|
||||
var location = ((req.headers['x-forwarded-proto'] === 'https' || socket.encrypted) ? 'wss' : 'ws') + '://' + wshost + req.url
|
||||
, protocol = req.headers['sec-websocket-protocol'];
|
||||
|
||||
// handshake completion code to run once nonce has been successfully retrieved
|
||||
var completeHandshake = function(nonce, rest) {
|
||||
// calculate key
|
||||
var k1 = req.headers['sec-websocket-key1']
|
||||
, k2 = req.headers['sec-websocket-key2']
|
||||
, md5 = crypto.createHash('md5');
|
||||
|
||||
[k1, k2].forEach(function (k) {
|
||||
var n = parseInt(k.replace(/[^\d]/g, ''))
|
||||
, spaces = k.replace(/[^ ]/g, '').length;
|
||||
if (spaces === 0 || n % spaces !== 0){
|
||||
abortConnection(socket, 400, 'Bad Request');
|
||||
return;
|
||||
}
|
||||
n /= spaces;
|
||||
md5.update(String.fromCharCode(
|
||||
n >> 24 & 0xFF,
|
||||
n >> 16 & 0xFF,
|
||||
n >> 8 & 0xFF,
|
||||
n & 0xFF));
|
||||
});
|
||||
md5.update(nonce.toString('binary'));
|
||||
|
||||
var headers = [
|
||||
'HTTP/1.1 101 Switching Protocols'
|
||||
, 'Upgrade: WebSocket'
|
||||
, 'Connection: Upgrade'
|
||||
, 'Sec-WebSocket-Location: ' + location
|
||||
];
|
||||
if (typeof protocol != 'undefined') headers.push('Sec-WebSocket-Protocol: ' + protocol);
|
||||
if (typeof origin != 'undefined') headers.push('Sec-WebSocket-Origin: ' + origin);
|
||||
|
||||
socket.setTimeout(0);
|
||||
socket.setNoDelay(true);
|
||||
try {
|
||||
// merge header and hash buffer
|
||||
var headerBuffer = new Buffer(headers.concat('', '').join('\r\n'));
|
||||
var hashBuffer = new Buffer(md5.digest('binary'), 'binary');
|
||||
var handshakeBuffer = new Buffer(headerBuffer.length + hashBuffer.length);
|
||||
headerBuffer.copy(handshakeBuffer, 0);
|
||||
hashBuffer.copy(handshakeBuffer, headerBuffer.length);
|
||||
|
||||
// do a single write, which - upon success - causes a new client websocket to be setup
|
||||
socket.write(handshakeBuffer, 'binary', function(err) {
|
||||
if (err) return; // do not create client if an error happens
|
||||
var client = new WebSocket([req, socket, rest], {
|
||||
protocolVersion: 'hixie-76',
|
||||
protocol: protocol
|
||||
});
|
||||
if (self.options.clientTracking) {
|
||||
self.clients.push(client);
|
||||
client.on('close', function() {
|
||||
var index = self.clients.indexOf(client);
|
||||
if (index != -1) {
|
||||
self.clients.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// signal upgrade complete
|
||||
socket.removeListener('error', errorHandler);
|
||||
cb(client);
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
try { socket.destroy(); } catch (e) {}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// retrieve nonce
|
||||
var nonceLength = 8;
|
||||
if (upgradeHead && upgradeHead.length >= nonceLength) {
|
||||
var nonce = upgradeHead.slice(0, nonceLength);
|
||||
var rest = upgradeHead.length > nonceLength ? upgradeHead.slice(nonceLength) : null;
|
||||
completeHandshake.call(self, nonce, rest);
|
||||
}
|
||||
else {
|
||||
// nonce not present in upgradeHead, so we must wait for enough data
|
||||
// data to arrive before continuing
|
||||
var nonce = new Buffer(nonceLength);
|
||||
upgradeHead.copy(nonce, 0);
|
||||
var received = upgradeHead.length;
|
||||
var rest = null;
|
||||
var handler = function (data) {
|
||||
var toRead = Math.min(data.length, nonceLength - received);
|
||||
if (toRead === 0) return;
|
||||
data.copy(nonce, received, 0, toRead);
|
||||
received += toRead;
|
||||
if (received == nonceLength) {
|
||||
socket.removeListener('data', handler);
|
||||
if (toRead < data.length) rest = data.slice(toRead);
|
||||
completeHandshake.call(self, nonce, rest);
|
||||
}
|
||||
}
|
||||
socket.on('data', handler);
|
||||
}
|
||||
}
|
||||
|
||||
// verify client
|
||||
if (typeof this.options.verifyClient == 'function') {
|
||||
var info = {
|
||||
origin: origin,
|
||||
secure: typeof req.connection.authorized !== 'undefined' || typeof req.connection.encrypted !== 'undefined',
|
||||
req: req
|
||||
};
|
||||
if (this.options.verifyClient.length == 2) {
|
||||
var self = this;
|
||||
this.options.verifyClient(info, function(result, code, name) {
|
||||
if (typeof code === 'undefined') code = 401;
|
||||
if (typeof name === 'undefined') name = http.STATUS_CODES[code];
|
||||
|
||||
if (!result) abortConnection(socket, code, name);
|
||||
else onClientVerified.apply(self);
|
||||
});
|
||||
return;
|
||||
}
|
||||
else if (!this.options.verifyClient(info)) {
|
||||
abortConnection(socket, 401, 'Unauthorized');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// no client verification required
|
||||
onClientVerified();
|
||||
}
|
||||
|
||||
function acceptExtensions(offer) {
|
||||
var extensions = {};
|
||||
var options = this.options.perMessageDeflate;
|
||||
if (options && offer[PerMessageDeflate.extensionName]) {
|
||||
var perMessageDeflate = new PerMessageDeflate(options !== true ? options : {}, true);
|
||||
perMessageDeflate.accept(offer[PerMessageDeflate.extensionName]);
|
||||
extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
function abortConnection(socket, code, name) {
|
||||
try {
|
||||
var response = [
|
||||
'HTTP/1.1 ' + code + ' ' + name,
|
||||
'Content-type: text/html'
|
||||
];
|
||||
socket.write(response.concat('', '').join('\r\n'));
|
||||
}
|
||||
catch (e) { /* ignore errors - we've aborted this connection */ }
|
||||
finally {
|
||||
// ensure that an early aborted connection is shut down completely
|
||||
try { socket.destroy(); } catch (e) {}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"author": {
|
||||
"name": "Einar Otto Stangvik",
|
||||
"email": "einaros@gmail.com",
|
||||
"url": "http://2x.io"
|
||||
},
|
||||
"name": "ws",
|
||||
"description": "simple to use, blazing fast and thoroughly tested websocket client, server and console for node.js, up-to-date against RFC-6455",
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"Hixie",
|
||||
"HyBi",
|
||||
"Push",
|
||||
"RFC-6455",
|
||||
"WebSocket",
|
||||
"WebSockets",
|
||||
"real-time"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/websockets/ws.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "make test"
|
||||
},
|
||||
"dependencies": {
|
||||
"options": ">=0.0.5",
|
||||
"ultron": "1.0.x"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ansi": "0.3.x",
|
||||
"benchmark": "0.3.x",
|
||||
"bufferutil": "1.2.x",
|
||||
"expect.js": "0.3.x",
|
||||
"mocha": "2.3.x",
|
||||
"should": "8.0.x",
|
||||
"tinycolor": "0.0.x",
|
||||
"utf-8-validate": "1.2.x"
|
||||
},
|
||||
"gypfile": true,
|
||||
"gitHead": "40a9d686288b5d0be13f2bf2f3f5da07afc8cda2",
|
||||
"bugs": {
|
||||
"url": "https://github.com/websockets/ws/issues"
|
||||
},
|
||||
"homepage": "https://github.com/websockets/ws#readme",
|
||||
"_id": "ws@1.0.1",
|
||||
"_shasum": "7d0b2a2e58cddd819039c29c9de65045e1b310e9",
|
||||
"_from": "ws@latest",
|
||||
"_npmVersion": "3.5.1",
|
||||
"_nodeVersion": "4.2.3",
|
||||
"_npmUser": {
|
||||
"name": "3rdeden",
|
||||
"email": "npm@3rd-Eden.com"
|
||||
},
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "einaros",
|
||||
"email": "einaros@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "v1",
|
||||
"email": "info@3rd-Eden.com"
|
||||
},
|
||||
{
|
||||
"name": "3rdeden",
|
||||
"email": "npm@3rd-Eden.com"
|
||||
}
|
||||
],
|
||||
"dist": {
|
||||
"shasum": "7d0b2a2e58cddd819039c29c9de65045e1b310e9",
|
||||
"tarball": "http://registry.npmjs.org/ws/-/ws-1.0.1.tgz"
|
||||
},
|
||||
"directories": {},
|
||||
"_resolved": "https://registry.npmjs.org/ws/-/ws-1.0.1.tgz"
|
||||
}
|
Loading…
Reference in New Issue