Commit fe0541a9 authored by Ximin Luo's avatar Ximin Luo

Imported Upstream version 1.1.0+ds1.e6ddaae4

parent 44e92c07
......@@ -3,5 +3,6 @@ node_modules
.*.swp
.lock-*
build
coverage
builderror.log
ALL_TESTS = $(shell find test/ -name '*.test.js')
ALL_INTEGRATION = $(shell find test/ -name '*.integration.js')
all:
node-gyp configure build
clean:
node-gyp clean
run-tests:
@./node_modules/.bin/mocha \
-t 5000 \
......@@ -21,12 +15,23 @@ run-integrationtests:
$(TESTFLAGS) \
$(TESTS)
run-coverage:
@./node_modules/.bin/istanbul cover --report html \
./node_modules/.bin/_mocha -- \
-t 5000 \
-s 6000 \
$(TESTFLAGS) \
$(TESTS)
test:
@$(MAKE) NODE_TLS_REJECT_UNAUTHORIZED=0 NODE_PATH=lib TESTS="$(ALL_TESTS)" run-tests
integrationtest:
@$(MAKE) NODE_TLS_REJECT_UNAUTHORIZED=0 NODE_PATH=lib TESTS="$(ALL_INTEGRATION)" run-integrationtests
coverage:
@$(MAKE) NODE_TLS_REJECT_UNAUTHORIZED=0 NODE_PATH=lib TESTS="$(ALL_TESTS)" run-coverage
benchmark:
@node bench/sender.benchmark.js
@node bench/parser.benchmark.js
......@@ -37,4 +42,4 @@ autobahn:
autobahn-server:
@NODE_PATH=lib node test/autobahn-server.js
.PHONY: test
.PHONY: test coverage
......@@ -31,13 +31,13 @@ compiler is installed on the host system.
- `npm install --save bufferutil`: Improves internal buffer operations which
allows for faster processing of masked WebSocket frames and general buffer
operations.
operations.
- `npm install --save utf-8-validate`: The specification requires validation of
invalid UTF-8 chars, some of these validations could not be done in JavaScript
hence the need for a binary addon. In most cases you will already be
validating the input that you receive for security purposes leading to double
validation. But if you want to be 100% spec conform and fast validation of UTF-8
then this module is a must.
validation. But if you want to be 100% spec-conforming and have fast
validation of UTF-8 then this module is a must.
### Sending and receiving text data
......@@ -110,7 +110,7 @@ wss.on('connection', function connection(ws) {
var location = url.parse(ws.upgradeReq.url, true);
// you might use location.query.access_token to authenticate or share sessions
// or ws.upgradeReq.headers.cookie (see http://stackoverflow.com/a/16395220/151312)
ws.on('message', function incoming(message) {
console.log('received: %s', message);
});
......@@ -161,7 +161,7 @@ catch (e) { /* handle error */ }
```js
var WebSocket = require('ws');
var ws = new WebSocket('ws://echo.websocket.org/', {
protocolVersion: 8,
protocolVersion: 8,
origin: 'http://websocket.org'
});
......@@ -183,13 +183,6 @@ ws.on('message', function message(data, flags) {
});
```
### Browserify users
When including ws via a browserify bundle, ws returns global.WebSocket which has slightly different API.
You should use the standard WebSockets API instead.
https://developer.mozilla.org/en-US/docs/WebSockets/Writing_WebSocket_client_applications#Availability_of_WebSockets
### Other examples
For a full example with a browser client communicating with a ws server, see the
......
# Security Guidelines
Please contact us directly at **security@3rd-Eden.com** for any bug that might
impact the security of this project. Please prefix the subject of your email
with `[security]` in lowercase and square brackets. Our email filters will
automatically prevent these messages from being moved to our spam box.
You will receive an acknowledgement of your report within **24 hours**.
All emails that do not include security vulnerabilities will be removed and
blocked instantly.
## Exceptions
If you do not receive an acknowledgement within the said time frame please give
us the benefit of the doubt as it's possible that we haven't seen it yet. In
this case please send us a message **without details** using one of the
following methods:
- Contact the lead developers of this project on their personal e-mails. You
can find the e-mails in the git logs, for example using the following command:
`git --no-pager show -s --format='%an <%ae>' <gitsha>` where `<gitsha>` is the
SHA1 of their latest commit in the project.
- Create a GitHub issue stating contact details and the severity of the issue.
Once we have acknowledged receipt of your report and confirmed the bug
ourselves we will work with you to fix the vulnerability and publicly acknowledge
your responsible disclosure, if you wish. In addition to that we will report
all vulnerabilities to the [Node Security Project](https://nodesecurity.io/).
## History
04 Jan 2016: [Buffer vulnerablity](https://github.com/websockets/ws/releases/tag/1.0.1)
......@@ -66,9 +66,9 @@ If `handleProtocols` is not set then the handshake is accepted regardless the va
If a property is empty then either an offered configuration or a default value is used.
### server.close()
### server.close([callback])
Close the server and terminate all clients
Close the server and terminate all clients, calls callback when done with an error if one occured.
### server.handleUpgrade(request, socket, upgradeHead, callback)
......
......@@ -4,7 +4,7 @@
* MIT Licensed
*/
module.exports.BufferUtil = {
exports.BufferUtil = {
merge: function(mergedBuffer, buffers) {
var offset = 0;
for (var i = 0, l = buffers.length; i < l; ++i) {
......
......@@ -11,7 +11,7 @@ PerMessageDeflate.extensionName = 'permessage-deflate';
* Per-message Compression Extensions implementation
*/
function PerMessageDeflate(options, isServer) {
function PerMessageDeflate(options, isServer,maxPayload) {
if (this instanceof PerMessageDeflate === false) {
throw new TypeError("Classes can't be function-called");
}
......@@ -21,6 +21,7 @@ function PerMessageDeflate(options, isServer) {
this._inflate = null;
this._deflate = null;
this.params = null;
this._maxPayload = maxPayload || 0;
}
/**
......@@ -236,6 +237,7 @@ PerMessageDeflate.prototype.decompress = function (data, fin, callback) {
var self = this;
var buffers = [];
var cumulativeBufferLength=0;
this._inflate.on('error', onError).on('data', onData);
this._inflate.write(data);
......@@ -253,7 +255,17 @@ PerMessageDeflate.prototype.decompress = function (data, fin, callback) {
}
function onData(data) {
buffers.push(data);
if(self._maxPayload!==undefined && self._maxPayload!==null && self._maxPayload>0){
cumulativeBufferLength+=data.length;
if(cumulativeBufferLength>self._maxPayload){
buffers=[];
cleanup();
var err={type:1009};
callback(err);
return;
}
}
buffers.push(data);
}
function cleanup() {
......
......@@ -47,6 +47,7 @@ module.exports = Receiver;
*/
Receiver.prototype.add = function(data) {
if (this.dead) return;
var self = this;
function doAdd() {
if (self.state === EMPTY) {
......@@ -153,8 +154,17 @@ Receiver.prototype.parse = function() {
*/
Receiver.prototype.error = function (reason, terminate) {
if (this.dead) return;
this.reset();
this.onerror(reason, terminate);
if(typeof reason == 'string'){
this.onerror(new Error(reason), terminate);
}
else if(reason.constructor == Error){
this.onerror(reason, terminate);
}
else{
this.onerror(new Error("An error occured"),terminate);
}
return this;
};
......
......@@ -15,10 +15,15 @@ var util = require('util')
* HyBi Receiver implementation
*/
function Receiver (extensions) {
function Receiver (extensions,maxPayload) {
if (this instanceof Receiver === false) {
throw new TypeError("Classes can't be function-called");
}
if(typeof extensions==='number'){
maxPayload=extensions;
extensions={};
}
// memory pool for fragmented messages
var fragmentedPoolPrevUsed = -1;
......@@ -39,8 +44,9 @@ function Receiver (extensions) {
Math.ceil((unfragmentedPoolPrevUsed + db.used) / 2) :
db.used;
});
this.extensions = extensions || {};
this.maxPayload = maxPayload || 0;
this.currentPayloadLength = 0;
this.state = {
activeFragmentedOperation: null,
lastFragment: false,
......@@ -54,6 +60,7 @@ function Receiver (extensions) {
this.expectBuffer = null;
this.expectHandler = null;
this.currentMessage = [];
this.currentMessageLength = 0;
this.messageHandlers = [];
this.expectHeader(2, this.processPacket);
this.dead = false;
......@@ -76,6 +83,7 @@ module.exports = Receiver;
*/
Receiver.prototype.add = function(data) {
if (this.dead) return;
var dataLength = data.length;
if (dataLength == 0) return;
if (this.expectBuffer == null) {
......@@ -244,6 +252,7 @@ Receiver.prototype.processPacket = function (data) {
*/
Receiver.prototype.endPacket = function() {
if (this.dead) return;
if (!this.state.fragmentedOperation) this.unfragmentedBufferPool.reset(true);
else if (this.state.lastFragment) this.fragmentedBufferPool.reset(true);
this.expectOffset = 0;
......@@ -253,6 +262,7 @@ Receiver.prototype.endPacket = function() {
// end current fragmented operation
this.state.activeFragmentedOperation = null;
}
this.currentPayloadLength = 0;
this.state.lastFragment = false;
this.state.opcode = this.state.activeFragmentedOperation != null ? this.state.activeFragmentedOperation : 0;
this.state.masked = false;
......@@ -281,7 +291,9 @@ Receiver.prototype.reset = function() {
this.expectHandler = null;
this.overflow = [];
this.currentMessage = [];
this.currentMessageLength = 0;
this.messageHandlers = [];
this.currentPayloadLength = 0;
};
/**
......@@ -296,20 +308,6 @@ Receiver.prototype.unmask = function (mask, buf, binary) {
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
*
......@@ -317,8 +315,17 @@ Receiver.prototype.concatBuffers = function(buffers) {
*/
Receiver.prototype.error = function (reason, protocolErrorCode) {
if (this.dead) return;
this.reset();
this.onerror(reason, protocolErrorCode);
if(typeof reason == 'string'){
this.onerror(new Error(reason), protocolErrorCode);
}
else if(reason.constructor == Error){
this.onerror(reason, protocolErrorCode);
}
else{
this.onerror(new Error("An error occured"),protocolErrorCode);
}
return this;
};
......@@ -365,6 +372,27 @@ Receiver.prototype.applyExtensions = function(messageBuffer, fin, compressed, ca
}
};
/**
* Checks payload size, disconnects socket when it exceeds maxPayload
*
* @api private
*/
Receiver.prototype.maxPayloadExceeded = function(length) {
if (this.maxPayload=== undefined || this.maxPayload === null || this.maxPayload < 1) {
return false;
}
var fullLength = this.currentPayloadLength + length;
if (fullLength < this.maxPayload) {
this.currentPayloadLength = fullLength;
return false;
}
this.error('payload cannot exceed ' + this.maxPayload + ' bytes', 1009);
this.messageBuffer=[];
this.cleanup();
return true;
};
/**
* Buffer utilities
*/
......@@ -425,11 +453,20 @@ var opcodes = {
// decode length
var firstLength = data[1] & 0x7f;
if (firstLength < 126) {
if (self.maxPayloadExceeded(firstLength)){
self.error('Maximumpayload exceeded in compressed text message. Aborting...', 1009);
return;
}
opcodes['1'].getData.call(self, firstLength);
}
else if (firstLength == 126) {
self.expectHeader(2, function(data) {
opcodes['1'].getData.call(self, readUInt16BE.call(data, 0));
var length = readUInt16BE.call(data, 0);
if (self.maxPayloadExceeded(length)){
self.error('Maximumpayload exceeded in compressed text message. Aborting...', 1009);
return;
}
opcodes['1'].getData.call(self, length);
});
}
else if (firstLength == 127) {
......@@ -438,6 +475,11 @@ var opcodes = {
self.error('packets with length spanning more than 32 bit is currently not supported', 1008);
return;
}
var length = readUInt32BE.call(data, 4);
if (self.maxPayloadExceeded(length)){
self.error('Maximumpayload exceeded in compressed text message. Aborting...', 1009);
return;
}
opcodes['1'].getData.call(self, readUInt32BE.call(data, 4));
});
}
......@@ -464,12 +506,29 @@ var opcodes = {
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 (err) {
if(err.type===1009){
return self.error('Maximumpayload exceeded in compressed text message. Aborting...', 1009);
}
return self.error(err.message, 1007);
}
if (buffer != null) {
if( self.maxPayload==0 || (self.maxPayload > 0 && (self.currentMessageLength + buffer.length) < self.maxPayload) ){
self.currentMessage.push(buffer);
}
else{
self.currentMessage=null;
self.currentMessage = [];
self.currentMessageLength = 0;
self.error(new Error('Maximum payload exceeded. maxPayload: '+self.maxPayload), 1009);
return;
}
self.currentMessageLength += buffer.length;
}
if (state.lastFragment) {
var messageBuffer = self.concatBuffers(self.currentMessage);
var messageBuffer = Buffer.concat(self.currentMessage);
self.currentMessage = [];
self.currentMessageLength = 0;
if (!Validation.isValidUTF8(messageBuffer)) {
self.error('invalid utf8 sequence', 1007);
return;
......@@ -490,11 +549,20 @@ var opcodes = {
// decode length
var firstLength = data[1] & 0x7f;
if (firstLength < 126) {
if (self.maxPayloadExceeded(firstLength)){
self.error('Max payload exceeded in compressed text message. Aborting...', 1009);
return;
}
opcodes['2'].getData.call(self, firstLength);
}
else if (firstLength == 126) {
self.expectHeader(2, function(data) {
opcodes['2'].getData.call(self, readUInt16BE.call(data, 0));
var length = readUInt16BE.call(data, 0);
if (self.maxPayloadExceeded(length)){
self.error('Max payload exceeded in compressed text message. Aborting...', 1009);
return;
}
opcodes['2'].getData.call(self, length);
});
}
else if (firstLength == 127) {
......@@ -503,7 +571,12 @@ var opcodes = {
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));
var length = readUInt32BE.call(data, 4, true);
if (self.maxPayloadExceeded(length)){
self.error('Max payload exceeded in compressed text message. Aborting...', 1009);
return;
}
opcodes['2'].getData.call(self, length);
});
}
},
......@@ -529,11 +602,29 @@ var opcodes = {
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 (err) {
if(err.type===1009){
return self.error('Max payload exceeded in compressed binary message. Aborting...', 1009);
}
return self.error(err.message, 1007);
}
if (buffer != null) {
if( self.maxPayload==0 || (self.maxPayload > 0 && (self.currentMessageLength + buffer.length) < self.maxPayload) ){
self.currentMessage.push(buffer);
}
else{
self.currentMessage=null;
self.currentMessage = [];
self.currentMessageLength = 0;
self.error(new Error('Maximum payload exceeded'), 1009);
return;
}
self.currentMessageLength += buffer.length;
}
if (state.lastFragment) {
var messageBuffer = self.concatBuffers(self.currentMessage);
var messageBuffer = Buffer.concat(self.currentMessage);
self.currentMessage = [];
self.currentMessageLength = 0;
self.onbinary(messageBuffer, {masked: state.masked, buffer: messageBuffer});
}
callback();
......
......@@ -197,7 +197,7 @@ Sender.prototype.frameAndSend = function(opcode, data, finalFragment, maskData,
if (maskData) {
outputBuffer[1] = secondByte | 0x80;
var mask = this._randomMask || (this._randomMask = getRandomMask());
var mask = getRandomMask();
outputBuffer[dataOffset - 4] = mask[0];
outputBuffer[dataOffset - 3] = mask[1];
outputBuffer[dataOffset - 2] = mask[2];
......
......@@ -3,10 +3,9 @@
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
* MIT Licensed
*/
module.exports.Validation = {
exports.Validation = {
isValidUTF8: function(buffer) {
return true;
}
};
......@@ -71,6 +71,7 @@ function WebSocket(address, protocols, options) {
this.readyState = null;
this.supports = {};
this.extensions = {};
this._binaryType = 'nodebuffer';
if (Array.isArray(address)) {
initAsServerClient.apply(this, address.concat(options));
......@@ -371,6 +372,27 @@ Object.defineProperty(WebSocket.prototype, 'bufferedAmount', {
}
});
/**
* Expose binaryType
*
* This deviates from the W3C interface since ws doesn't support the required
* default "blob" type (instead we define a custom "nodebuffer" type).
*
* @see http://dev.w3.org/html5/websockets/#the-websocket-interface
* @api public
*/
Object.defineProperty(WebSocket.prototype, 'binaryType', {
get: function get() {
return this._binaryType;
},
set: function set(type) {
if (type === 'arraybuffer' || type === 'nodebuffer')
this._binaryType = type;
else
throw new SyntaxError('unsupported binaryType: must be either "nodebuffer" or "arraybuffer"');
}
});
/**
* Emulates the W3C Browser based WebSocket interface using function members.
*
......@@ -415,6 +437,8 @@ WebSocket.prototype.addEventListener = function(method, listener) {
var target = this;
function onMessage (data, flags) {
if (flags.binary && this.binaryType === 'arraybuffer')
data = new Uint8Array(data).buffer;
listener.call(target, new MessageEvent(data, !!flags.binary, target));
}
......@@ -523,7 +547,8 @@ function initAsServerClient(req, socket, upgradeHead, options) {
options = new Options({
protocolVersion: protocolVersion,
protocol: null,
extensions: {}
extensions: {},
maxPayload: 0
}).merge(options);
// expose state properties
......@@ -534,7 +559,7 @@ function initAsServerClient(req, socket, upgradeHead, options) {
this.upgradeReq = req;
this.readyState = WebSocket.CONNECTING;
this._isServer = true;
this.maxPayload = options.value.maxPayload;
// establish connection
if (options.value.protocolVersion === 'hixie-76') {
establishConnection.call(this, ReceiverHixie, SenderHixie, socket, upgradeHead);
......@@ -770,7 +795,7 @@ function establishConnection(ReceiverClass, SenderClass, socket, upgradeHead) {
socket.setTimeout(0);
socket.setNoDelay(true);
this._receiver = new ReceiverClass(this.extensions);
this._receiver = new ReceiverClass(this.extensions,this.maxPayload);
this._socket = socket;
// socket cleanup handlers
......@@ -848,7 +873,7 @@ function establishConnection(ReceiverClass, SenderClass, socket, upgradeHead) {
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);
self.emit('error', (reason instanceof Error) ? reason : (new Error(reason)));
};
// finalize the client
......@@ -911,21 +936,18 @@ function sendStream(instance, stream, options, cb) {
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 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();
......
......@@ -36,7 +36,8 @@ function WebSocketServer(options, callback) {
noServer: false,
disableHixie: false,
clientTracking: true,
perMessageDeflate: true
perMessageDeflate: true,
maxPayload: null
}).merge(options);
if (!options.isDefinedAndNonNull('port') && !options.isDefinedAndNonNull('server') && !options.value.noServer) {
......@@ -72,13 +73,15 @@ function WebSocketServer(options, callback) {
this._server._webSocketPaths[options.value.path] = 1;
}
}
if (this._server) this._server.once('listening', function() { self.emit('listening'); });
if (this._server) {
this._onceServerListening = function() { self.emit('listening'); };
this._server.once('listening', this._onceServerListening);
}
if (typeof this._server != 'undefined') {
this._server.on('error', function(error) {
self.emit('error', error)
});
this._server.on('upgrade', function(req, socket, upgradeHead) {
this._onServerError = function(error) { self.emit('error', error) };
this._server.on('error', this._onServerError);
this._onServerUpgrade = 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);
......@@ -87,7 +90,8 @@ function WebSocketServer(options, callback) {
self.emit('connection'+req.url, client);
self.emit('connection', client);
});
});
};
this._server.on('upgrade', this._onServerUpgrade);
}
this.options = options.value;
......@@ -134,6 +138,11 @@ WebSocketServer.prototype.close = function(callback) {
}
}
finally {
if (this._server) {
this._server.removeListener('listening', this._onceServerListening);
this._server.removeListener('error', this._onServerError);
this._server.removeListener('upgrade', this._onServerUpgrade);
}
delete this._server;
}
if(callback)
......@@ -256,7 +265,8 @@ function handleHybiUpgrade(req, socket, upgradeHead, cb) {
var client = new WebSocket([req, socket, upgradeHead], {
protocolVersion: version,
protocol: protocol,
extensions: extensions
extensions: extensions,
maxPayload: self.options.maxPayload
});
if (self.options.clientTracking) {
......@@ -359,8 +369,42 @@ function handleHixieUpgrade(req, socket, upgradeHead, cb) {
var location = ((req.headers['x-forwarded-proto'] === 'https' || socket.encrypted) ? 'wss' : 'ws') + '://' + wshost + req.url
, protocol = req.headers['sec-websocket-protocol'];
// build the response header and return a Buffer
var buildResponseHeader = function() {
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);
return new Buffer(headers.concat('', '').join('\r\n'));
};
// send handshake response before receiving the nonce
var handshakeResponse = function() {
socket.setTimeout(0);
socket.setNoDelay(true);
var headerBuffer = buildResponseHeader();
try {
socket.write(headerBuffer, 'binary', function(err) {
// remove listener if there was an error
if (err) socket.removeListener('data', handler);
return;
});
} catch (e) {
try { socket.destroy(); } catch (e) {}
return;
};
};
// handshake completion code to run once nonce has been successfully retrieved
var completeHandshake = function(nonce, rest) {
var completeHandshake = function(nonce, rest, headerBuffer) {
// calculate key
var k1 = req.headers['sec-websocket-key1']
, k2 = req.headers['sec-websocket-key2']
......@@ -382,20 +426,10 @@ function handleHixieUpgrade(req, socket, upgradeHead, cb) {
});
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);