Comparing version 0.4.7 to 0.4.8
@@ -0,1 +1,7 @@ | ||
v0.4.8 - Feb 29th 2012 | ||
===================== | ||
* Allow verifyClient to run asynchronously [karlsequin] | ||
* Various bugfixes and cleanups. [einaros] | ||
v0.4.7 - Feb 21st 2012 | ||
@@ -2,0 +8,0 @@ ===================== |
@@ -19,9 +19,3 @@ /*! | ||
function Sender (socket, options) { | ||
options = new Options({ | ||
sendBufferCacheSize: 65536 | ||
}).merge(options); | ||
if (options.value.sendBufferCacheSize > 0) { | ||
this._sendCacheSize = options.value.sendBufferCacheSize; | ||
this._sendCache = new Buffer(this._sendCacheSize); | ||
} | ||
options = new Options({}).merge(options); | ||
this._socket = socket; | ||
@@ -100,3 +94,4 @@ this.firstFragment = true; | ||
Sender.prototype.frameAndSend = function(opcode, data, finalFragment, maskData, cb) { | ||
var dontModifyData = true; | ||
var canModifyData = false; | ||
if (!data) { | ||
@@ -112,9 +107,12 @@ try { | ||
} | ||
else if (!Buffer.isBuffer(data)) { | ||
dontModifyData = false; | ||
if (!Buffer.isBuffer(data)) { | ||
canModifyData = true; | ||
data = (data && typeof data.buffer !== 'undefined') ? getArrayBuffer(data.buffer) : new Buffer(data); | ||
} | ||
var dataLength = data.length | ||
, dataOffset = maskData ? 6 : 2 | ||
, secondByte = dataLength; | ||
if (dataLength >= 65536) { | ||
@@ -128,7 +126,8 @@ dataOffset += 8; | ||
} | ||
var totalLength = (maskData && dontModifyData) ? dataLength + dataOffset : dataOffset; | ||
var outputBuffer = (this._sendCache && totalLength <= this._sendCacheSize) | ||
? ((totalLength == this._sendCacheSize ? this._sendCache : this._sendCache.slice(0, totalLength))) | ||
: new Buffer(totalLength); | ||
var totalLength = (maskData && !canModifyData) ? dataLength + dataOffset : dataOffset; | ||
var outputBuffer = new Buffer(totalLength); | ||
outputBuffer[0] = finalFragment ? opcode | 0x80 : opcode; | ||
switch (secondByte) { | ||
@@ -142,4 +141,6 @@ case 126: | ||
} | ||
var sendsDone = 0; | ||
var cbCaller = function() { if (++sendsDone == 2 && typeof cb == 'function') cb(null); } | ||
if (maskData) { | ||
@@ -152,3 +153,3 @@ outputBuffer[1] = secondByte | 0x80; | ||
outputBuffer[dataOffset - 1] = mask[3]; | ||
if (dontModifyData) { | ||
if (!canModifyData) { | ||
bufferUtil.mask(data, mask, outputBuffer, dataOffset, dataLength); | ||
@@ -155,0 +156,0 @@ try { |
@@ -99,3 +99,3 @@ /*! | ||
WebSocket.prototype.close = function(code, data) { | ||
if (this.readyState == WebSocket.CLOSING) return; | ||
if (this.readyState == WebSocket.CLOSING || this.readyState == WebSocket.CLOSED) return; | ||
if (this.readyState == WebSocket.CONNECTING) { | ||
@@ -105,3 +105,2 @@ this._readyState = WebSocket.CLOSED; | ||
} | ||
if (this.readyState != WebSocket.OPEN) throw new Error('not opened'); | ||
try { | ||
@@ -136,7 +135,11 @@ this._readyState = WebSocket.CLOSING; | ||
* @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(data, options) { | ||
if (this.readyState != WebSocket.OPEN) throw new Error('not opened'); | ||
WebSocket.prototype.ping = function(data, options, dontFailWhenClosed) { | ||
if (this.readyState != WebSocket.OPEN) { | ||
if (dontFailWhenClosed === true) return; | ||
throw new Error('not opened'); | ||
} | ||
options = options || {}; | ||
@@ -152,7 +155,11 @@ if (typeof options.mask == 'undefined') options.mask = !this._isServer; | ||
* @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) { | ||
if (this.readyState != WebSocket.OPEN) throw new Error('not opened'); | ||
WebSocket.prototype.pong = function(data, options, dontFailWhenClosed) { | ||
if (this.readyState != WebSocket.OPEN) { | ||
if (dontFailWhenClosed === true) return; | ||
throw new Error('not opened'); | ||
} | ||
options = options || {}; | ||
@@ -543,3 +550,3 @@ if (typeof options.mask == 'undefined') options.mask = !this._isServer; | ||
flags = flags || {}; | ||
self.pong(data, {mask: !self._isServer, binary: flags.binary === true}); | ||
self.pong(data, {mask: !self._isServer, binary: flags.binary === true}, true); | ||
self.emit('ping', data, flags); | ||
@@ -546,0 +553,0 @@ }); |
@@ -175,2 +175,4 @@ /*! | ||
req.headers['origin']; | ||
var args = [req, socket, upgradeHead, version, cb]; | ||
if (typeof this.options.verifyClient == 'function') { | ||
@@ -182,3 +184,11 @@ var info = { | ||
}; | ||
if (!this.options.verifyClient(info)) { | ||
if (this.options.verifyClient.length == 2) { | ||
var self = this; | ||
this.options.verifyClient(info, function(result) { | ||
if (!result) abortConnection(socket, 401, 'Unauthorized') | ||
else completeUpgrade.apply(self, args); | ||
}); | ||
return; | ||
} | ||
else if (!this.options.verifyClient(info)) { | ||
abortConnection(socket, 401, 'Unauthorized'); | ||
@@ -189,4 +199,8 @@ return; | ||
var protocol = req.headers['sec-websocket-protocol']; | ||
completeUpgrade.apply(this, args); | ||
} | ||
function completeUpgrade(req, socket, upgradeHead, version, cb) { | ||
var protocol = req.headers['sec-websocket-protocol']; | ||
// calc key | ||
@@ -243,2 +257,92 @@ var key = req.headers['sec-websocket-key']; | ||
// setup handshake completion to run after client has been verified | ||
var self = this; | ||
var onClientVerified = function() { | ||
var 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 { | ||
socket.write(headers.concat('', '').join('\r\n')); | ||
socket.write(md5.digest('binary'), 'binary'); | ||
} | ||
catch (e) { | ||
try { socket.end(); } catch (_) {} | ||
return; | ||
} | ||
var client = new WebSocket([req, socket, rest], { | ||
protocolVersion: 'hixie-76', | ||
protocol: protocol | ||
}); | ||
self._clients.push(client); | ||
client.on('close', function() { | ||
var index = self._clients.indexOf(client); | ||
if (index != -1) { | ||
self._clients.splice(index, 1); | ||
} | ||
}); | ||
cb(client); | ||
} | ||
// 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 | ||
@@ -253,98 +357,16 @@ var location = (socket.encrypted ? 'wss' : 'ws') + '://' + req.headers.host + req.url | ||
}; | ||
if (!this.options.verifyClient(info)) { | ||
abortConnection(socket, 401, 'Unauthorized'); | ||
if (this.options.verifyClient.length == 2) { | ||
var self = this; | ||
this.options.verifyClient(info, function(result) { | ||
if (!result) abortConnection(socket, 401, 'Unauthorized') | ||
else onClientVerified.apply(self); | ||
}); | ||
return; | ||
} | ||
} | ||
var protocol = req.headers['sec-websocket-protocol']; | ||
var completeHandshake = function(nonce, rest) { | ||
// calculate key | ||
var k1 = req.headers['sec-websocket-key1'] | ||
, k2 = req.headers['sec-websocket-key2'] | ||
, md5 = crypto.createHash('md5') | ||
, self = this; | ||
[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 { | ||
socket.write(headers.concat('', '').join('\r\n')); | ||
socket.write(md5.digest('binary'), 'binary'); | ||
} | ||
catch (e) { | ||
try { socket.end(); } catch (_) {} | ||
else if (!this.options.verifyClient(info)) { | ||
abortConnection(socket, 401, 'Unauthorized'); | ||
return; | ||
} | ||
var client = new WebSocket([req, socket, rest], { | ||
protocolVersion: 'hixie-76', | ||
protocol: protocol | ||
}); | ||
this._clients.push(client); | ||
var self = this; | ||
client.on('close', function() { | ||
var index = self._clients.indexOf(client); | ||
if (index != -1) { | ||
self._clients.splice(index, 1); | ||
} | ||
}); | ||
cb(client); | ||
} | ||
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(this, 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 self = this; | ||
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); | ||
} | ||
onClientVerified(); | ||
} | ||
@@ -351,0 +373,0 @@ |
@@ -5,3 +5,3 @@ { | ||
"description": "simple to use, blazing fast and thoroughly tested websocket client, server and console for node.js, up-to-date against RFC-6455", | ||
"version": "0.4.7", | ||
"version": "0.4.8", | ||
"repository": { | ||
@@ -29,4 +29,5 @@ "type": "git", | ||
"benchmark": "0.3.x", | ||
"tinycolor": "0.x" | ||
"tinycolor": "0.x", | ||
"ansi": "latest" | ||
} | ||
} |
var Sender = require('../lib/Sender'); | ||
require('should'); | ||
describe('Sender', function() { | ||
it('creates a send cache equal to options.sendBufferCacheSize', function() { | ||
var sender = new Sender(null, { | ||
sendBufferCacheSize: 10 | ||
}); | ||
sender._sendCache.length.should.eql(10); | ||
}); | ||
it('keeps a send cache equal to null if options.sendBufferCacheSize is 0', function() { | ||
var sender = new Sender(null, { | ||
sendBufferCacheSize: 0 | ||
}); | ||
(typeof sender._sendCache).should.eql('undefined'); | ||
}); | ||
it('keeps a send cache equal to null if options.sendBufferCacheSize is -1', function() { | ||
var sender = new Sender(null, { | ||
sendBufferCacheSize: -1 | ||
}); | ||
(typeof sender._sendCache).should.eql('undefined'); | ||
}); | ||
describe('Sender', function() { | ||
describe('#frameAndSend', function() { | ||
it('does not modify a masked binary buffer', function() { | ||
var sender = new Sender({ write: function() {} }); | ||
var sender = new Sender({ write: function() {} }); | ||
var buf = new Buffer([1, 2, 3, 4, 5]); | ||
@@ -39,3 +18,3 @@ sender.frameAndSend(2, buf, true, true); | ||
it('does not modify a masked text buffer', function() { | ||
var sender = new Sender({ write: function() {} }); | ||
var sender = new Sender({ write: function() {} }); | ||
var text = 'hi there'; | ||
@@ -42,0 +21,0 @@ sender.frameAndSend(1, text, true, true); |
@@ -290,2 +290,13 @@ var assert = require('assert') | ||
it('before connect can silently fail', function(done) { | ||
server.createServer(++port, function(srv) { | ||
var ws = new WebSocket('ws://localhost:' + port); | ||
ws.on('error', function() {}); | ||
ws.ping('', {}, true); | ||
srv.close(); | ||
ws.terminate(); | ||
done(); | ||
}); | ||
}); | ||
it('without message is successfully transmitted to the server', function(done) { | ||
@@ -338,2 +349,28 @@ server.createServer(++port, function(srv) { | ||
describe('#pong', function() { | ||
it('before connect should fail', function(done) { | ||
server.createServer(++port, function(srv) { | ||
var ws = new WebSocket('ws://localhost:' + port); | ||
ws.on('error', function() {}); | ||
try { | ||
ws.pong(); | ||
} | ||
catch (e) { | ||
srv.close(); | ||
ws.terminate(); | ||
done(); | ||
} | ||
}); | ||
}); | ||
it('before connect can silently fail', function(done) { | ||
server.createServer(++port, function(srv) { | ||
var ws = new WebSocket('ws://localhost:' + port); | ||
ws.on('error', function() {}); | ||
ws.pong('', {}, true); | ||
srv.close(); | ||
ws.terminate(); | ||
done(); | ||
}); | ||
}); | ||
it('without message is successfully transmitted to the server', function(done) { | ||
@@ -340,0 +377,0 @@ server.createServer(++port, function(srv) { |
@@ -505,2 +505,59 @@ var http = require('http') | ||
it('client can be denied asynchronously', function(done) { | ||
var wss = new WebSocketServer({port: ++port, verifyClient: function(o, cb) { | ||
cb(false); | ||
}}, function() { | ||
var options = { | ||
port: port, | ||
host: '127.0.0.1', | ||
headers: { | ||
'Connection': 'Upgrade', | ||
'Upgrade': 'websocket', | ||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', | ||
'Sec-WebSocket-Version': 8, | ||
'Sec-WebSocket-Origin': 'http://foobar.com' | ||
} | ||
}; | ||
var req = http.request(options); | ||
req.end(); | ||
req.on('response', function(res) { | ||
res.statusCode.should.eql(401); | ||
process.nextTick(function() { | ||
wss.close(); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
wss.on('connection', function(ws) { | ||
done(new Error('connection must not be established')); | ||
}); | ||
wss.on('error', function() {}); | ||
}); | ||
it('client can be accepted asynchronously', function(done) { | ||
var wss = new WebSocketServer({port: ++port, verifyClient: function(o, cb) { | ||
cb(true); | ||
}}, function() { | ||
var options = { | ||
port: port, | ||
host: '127.0.0.1', | ||
headers: { | ||
'Connection': 'Upgrade', | ||
'Upgrade': 'websocket', | ||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', | ||
'Sec-WebSocket-Version': 13, | ||
'Origin': 'http://foobar.com' | ||
} | ||
}; | ||
var req = http.request(options); | ||
req.end(); | ||
}); | ||
wss.on('connection', function(ws) { | ||
ws.terminate(); | ||
wss.close(); | ||
done(); | ||
}); | ||
wss.on('error', function() {}); | ||
}); | ||
it('handles messages passed along with the upgrade request (upgrade head)', function(done) { | ||
@@ -784,2 +841,60 @@ var wss = new WebSocketServer({port: ++port, verifyClient: function(o) { | ||
it('client can be denied asynchronously', function(done) { | ||
var wss = new WebSocketServer({port: ++port, verifyClient: function(o, cb) { | ||
cb(false); | ||
}}, function() { | ||
var options = { | ||
port: port, | ||
host: '127.0.0.1', | ||
headers: { | ||
'Connection': 'Upgrade', | ||
'Upgrade': 'WebSocket', | ||
'Origin': 'http://foobarbaz.com', | ||
'Sec-WebSocket-Key1': '3e6b263 4 17 80', | ||
'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' | ||
} | ||
}; | ||
var req = http.request(options); | ||
req.write('WjN}|M(6'); | ||
req.end(); | ||
req.on('response', function(res) { | ||
res.statusCode.should.eql(401); | ||
process.nextTick(function() { | ||
wss.close(); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
wss.on('connection', function(ws) { | ||
done(new Error('connection must not be established')); | ||
}); | ||
wss.on('error', function() {}); | ||
}); | ||
it('client can be accepted asynchronously', function(done) { | ||
var wss = new WebSocketServer({port: ++port, verifyClient: function(o, cb) { | ||
cb(true); | ||
}}, function() { | ||
var options = { | ||
port: port, | ||
host: '127.0.0.1', | ||
headers: { | ||
'Connection': 'Upgrade', | ||
'Upgrade': 'WebSocket', | ||
'Origin': 'http://foobarbaz.com', | ||
'Sec-WebSocket-Key1': '3e6b263 4 17 80', | ||
'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' | ||
} | ||
}; | ||
var req = http.request(options); | ||
req.write('WjN}|M(6'); | ||
req.end(); | ||
}); | ||
wss.on('connection', function(ws) { | ||
wss.close(); | ||
done(); | ||
}); | ||
wss.on('error', function() {}); | ||
}); | ||
it('handles messages passed along with the upgrade request (upgrade head)', function(done) { | ||
@@ -786,0 +901,0 @@ var wss = new WebSocketServer({port: ++port, verifyClient: function(o) { |
Sorry, the diff of this file is not supported yet
212077
54
5524
5