Comparing version 0.0.4 to 0.0.5
@@ -5,3 +5,4 @@ module.exports = { | ||
"router": require('./lib/WebSocketRouter'), | ||
"frame": require('./lib/WebSocketFrame') | ||
"frame": require('./lib/WebSocketFrame'), | ||
"request": require('./lib/WebSocketRequest') | ||
}; |
@@ -67,162 +67,160 @@ var extend = require('./utils').extend; | ||
extend(WebSocketClient.prototype, { | ||
connect: function(requestUrl, protocols, origin) { | ||
var s = this; | ||
if (!(protocols instanceof Array)) { | ||
protocols = []; | ||
WebSocketClient.prototype.connect = function(requestUrl, protocols, origin) { | ||
var s = this; | ||
if (!(protocols instanceof Array)) { | ||
protocols = []; | ||
} | ||
this.protocols = protocols; | ||
this.origin = origin; | ||
if (typeof(requestUrl) === 'string') { | ||
this.url = url.parse(requestUrl); | ||
} | ||
else { | ||
this.url = requestUrl; // in case an already parsed url is passed in. | ||
} | ||
if (!this.url.protocol) { | ||
throw new Error("You must specify a full WebSocket URL, including protocol."); | ||
} | ||
if (!this.url.host) { | ||
throw new Error("You must specify a full WebSocket URL, including hostname. Relative URLs are not supported."); | ||
} | ||
this.secure = (this.url.protocol === 'wss:'); | ||
// validate protocol characters: | ||
this.protocols.forEach(function(protocol, index, array) { | ||
for (var i=0; i < protocol.length; i ++) { | ||
var charCode = protocol.charCodeAt(i); | ||
var character = protocol.charAt(i); | ||
if (charCode < 0x0021 || charCode > 0x007E || protocolSeparators.indexOf(character) !== -1) { | ||
throw new Error("Protocol list contains invalid character '" + String.fromCharCode(charCode) + "'"); | ||
} | ||
} | ||
this.protocols = protocols; | ||
this.origin = origin; | ||
if (typeof(requestUrl) === 'string') { | ||
this.url = url.parse(requestUrl); | ||
} | ||
else { | ||
this.url = requestUrl; // in case an already parsed url is passed in. | ||
} | ||
if (!this.url.protocol) { | ||
throw new Error("You must specify a full WebSocket URL, including protocol."); | ||
} | ||
if (!this.url.host) { | ||
throw new Error("You must specify a full WebSocket URL, including hostname. Relative URLs are not supported."); | ||
} | ||
this.secure = (this.url.protocol === 'wss:'); | ||
// validate protocol characters: | ||
this.protocols.forEach(function(protocol, index, array) { | ||
for (var i=0; i < protocol.length; i ++) { | ||
var charCode = protocol.charCodeAt(i); | ||
var character = protocol.charAt(i); | ||
if (charCode < 0x0021 || charCode > 0x007E || protocolSeparators.indexOf(character) !== -1) { | ||
throw new Error("Protocol list contains invalid character '" + String.fromCharCode(charCode) + "'"); | ||
} | ||
} | ||
}); | ||
}); | ||
var defaultPorts = { | ||
'ws:': '80', | ||
'wss:': '443' | ||
}; | ||
var defaultPorts = { | ||
'ws:': '80', | ||
'wss:': '443' | ||
}; | ||
if (!this.url.port) { | ||
this.url.port = defaultPorts[this.url.protocol]; | ||
} | ||
var nonce = new Buffer(16); | ||
for (var i=0; i < 16; i++) { | ||
nonce[i] = Math.round(Math.random()*0xFF); | ||
} | ||
this.base64nonce = nonce.toString('base64'); | ||
var requestOptions = { | ||
host: this.url.hostname, | ||
port: this.url.port, | ||
method: 'GET', | ||
path: this.url.path | ||
}; | ||
// FIXME: Using old http.createClient interface since Node's new | ||
// Agent-based API is buggy. | ||
var client = this.httpClient = http.createClient(this.url.port, this.url.hostname); | ||
var reqHeaders = { | ||
'Upgrade': 'websocket', | ||
'Connection': 'Upgrade', | ||
'Sec-WebSocket-Version': '8', | ||
'Sec-WebSocket-Key': this.base64nonce, | ||
'Host': this.url.hostname | ||
}; | ||
if (this.protocols.length > 0) { | ||
reqHeaders['Sec-WebSocket-Protocol'] = this.protocols.join(', '); | ||
} | ||
if (this.origin) { | ||
reqHeaders['Sec-WebSocket-Origin'] = this.origin; | ||
} | ||
// TODO: Implement extensions | ||
if (!this.url.port) { | ||
this.url.port = defaultPorts[this.url.protocol]; | ||
} | ||
var nonce = new Buffer(16); | ||
for (var i=0; i < 16; i++) { | ||
nonce[i] = Math.round(Math.random()*0xFF); | ||
} | ||
this.base64nonce = nonce.toString('base64'); | ||
var requestOptions = { | ||
host: this.url.hostname, | ||
port: this.url.port, | ||
method: 'GET', | ||
path: this.url.path | ||
}; | ||
// FIXME: Using old http.createClient interface since Node's new | ||
// Agent-based API is buggy. | ||
var client = this.httpClient = http.createClient(this.url.port, this.url.hostname); | ||
var reqHeaders = { | ||
'Upgrade': 'websocket', | ||
'Connection': 'Upgrade', | ||
'Sec-WebSocket-Version': '8', | ||
'Sec-WebSocket-Key': this.base64nonce, | ||
'Host': this.url.hostname | ||
}; | ||
if (this.protocols.length > 0) { | ||
reqHeaders['Sec-WebSocket-Protocol'] = this.protocols.join(', '); | ||
} | ||
if (this.origin) { | ||
reqHeaders['Sec-WebSocket-Origin'] = this.origin; | ||
} | ||
// TODO: Implement extensions | ||
var handleRequestError = function(error) { | ||
s.emit('error', error); | ||
} | ||
client.on('error', handleRequestError); | ||
var handleRequestError = function(error) { | ||
s.emit('error', error); | ||
} | ||
client.on('error', handleRequestError); | ||
client.on('upgrade', function handleClientUpgrade(response, socket, head) { | ||
req.removeListener('error', handleRequestError); | ||
s.socket = socket; | ||
s.response = response; | ||
s.firstDataChunk = head; | ||
s.validateHandshake(); | ||
}); | ||
client.on('upgrade', function handleClientUpgrade(response, socket, head) { | ||
req.removeListener('error', handleRequestError); | ||
s.socket = socket; | ||
s.response = response; | ||
s.firstDataChunk = head; | ||
s.validateHandshake(); | ||
}); | ||
var req = this.request = client.request(this.url.path, reqHeaders); | ||
var req = this.request = client.request(this.url.path, reqHeaders); | ||
req.on('response', function(response) { | ||
s.failHandshake("Server responded with a non-101 status: " + response.statusCode); | ||
}); | ||
req.end(); | ||
}, | ||
req.on('response', function(response) { | ||
s.failHandshake("Server responded with a non-101 status: " + response.statusCode); | ||
}); | ||
validateHandshake: function() { | ||
var headers = this.response.headers; | ||
if (this.protocols.length > 0) { | ||
this.protocol = headers['sec-websocket-protocol']; | ||
if (this.protocol) { | ||
if (this.protocols.indexOf(this.protocol) === -1) { | ||
this.failHandshake("Server did not respond with a requested protocol."); | ||
return; | ||
} | ||
} | ||
else { | ||
this.failHandshake("Expected a Sec-WebSocket-Protocol header."); | ||
req.end(); | ||
}; | ||
WebSocketClient.prototype.validateHandshake = function() { | ||
var headers = this.response.headers; | ||
if (this.protocols.length > 0) { | ||
this.protocol = headers['sec-websocket-protocol']; | ||
if (this.protocol) { | ||
if (this.protocols.indexOf(this.protocol) === -1) { | ||
this.failHandshake("Server did not respond with a requested protocol."); | ||
return; | ||
} | ||
} | ||
if (!(headers['connection'] && headers['connection'].toLocaleLowerCase() === 'upgrade')) { | ||
this.failHandshake("Expected a Connection: Upgrade header from the server"); | ||
else { | ||
this.failHandshake("Expected a Sec-WebSocket-Protocol header."); | ||
return; | ||
} | ||
if (!(headers['upgrade'] && headers['upgrade'].toLocaleLowerCase() === 'websocket')) { | ||
this.failHandshake("Expected an Upgrade: websocket header from the server"); | ||
return; | ||
} | ||
var sha1 = crypto.createHash('sha1'); | ||
sha1.update(this.base64nonce + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); | ||
var expectedKey = sha1.digest('base64'); | ||
if (!headers['sec-websocket-accept']) { | ||
this.failHandshake("Expected Sec-WebSocket-Accept header from server") | ||
return; | ||
} | ||
if (!(headers['sec-websocket-accept'] === expectedKey)) { | ||
this.failHandshake("Sec-WebSocket-Accept header from server didn't match expected value of " + expectedKey); | ||
return; | ||
} | ||
// TODO: Support extensions | ||
this.succeedHandshake(); | ||
}, | ||
} | ||
failHandshake: function(errorDescription) { | ||
if (this.socket && this.socket.writable) { | ||
this.socket.end(); | ||
} | ||
this.emit('connectFailed', errorDescription); | ||
}, | ||
if (!(headers['connection'] && headers['connection'].toLocaleLowerCase() === 'upgrade')) { | ||
this.failHandshake("Expected a Connection: Upgrade header from the server"); | ||
return; | ||
} | ||
succeedHandshake: function() { | ||
var connection = new WebSocketConnection(this.socket, [], this.protocol, true, this.config); | ||
this.emit('connect', connection); | ||
if (this.firstDataChunk.length > 0) { | ||
connection.handleSocketData(this.firstDataChunk); | ||
this.firstDataChunk = null; | ||
} | ||
if (!(headers['upgrade'] && headers['upgrade'].toLocaleLowerCase() === 'websocket')) { | ||
this.failHandshake("Expected an Upgrade: websocket header from the server"); | ||
return; | ||
} | ||
}); | ||
var sha1 = crypto.createHash('sha1'); | ||
sha1.update(this.base64nonce + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); | ||
var expectedKey = sha1.digest('base64'); | ||
if (!headers['sec-websocket-accept']) { | ||
this.failHandshake("Expected Sec-WebSocket-Accept header from server") | ||
return; | ||
} | ||
if (!(headers['sec-websocket-accept'] === expectedKey)) { | ||
this.failHandshake("Sec-WebSocket-Accept header from server didn't match expected value of " + expectedKey); | ||
return; | ||
} | ||
// TODO: Support extensions | ||
this.succeedHandshake(); | ||
}; | ||
WebSocketClient.prototype.failHandshake = function(errorDescription) { | ||
if (this.socket && this.socket.writable) { | ||
this.socket.end(); | ||
} | ||
this.emit('connectFailed', errorDescription); | ||
}; | ||
WebSocketClient.prototype.succeedHandshake = function() { | ||
var connection = new WebSocketConnection(this.socket, [], this.protocol, true, this.config); | ||
this.emit('connect', connection); | ||
if (this.firstDataChunk.length > 0) { | ||
connection.handleSocketData(this.firstDataChunk); | ||
this.firstDataChunk = null; | ||
} | ||
}; | ||
module.exports = WebSocketClient; |
@@ -1,2 +0,1 @@ | ||
var extend = require('./utils').extend; | ||
var crypto = require('crypto'); | ||
@@ -80,128 +79,150 @@ var util = require('util'); | ||
extend(WebSocketConnection.prototype, { | ||
handleSocketData: function(data) { | ||
this.bufferList.write(data); | ||
WebSocketConnection.prototype.handleSocketData = function(data) { | ||
this.bufferList.write(data); | ||
// currentFrame.addData returns true if all data necessary to parse | ||
// the frame was available. It returns false if we are waiting for | ||
// more data to come in on the wire. | ||
while (this.connected && this.currentFrame.addData(this.bufferList)) { | ||
// currentFrame.addData returns true if all data necessary to parse | ||
// the frame was available. It returns false if we are waiting for | ||
// more data to come in on the wire. | ||
while (this.connected && this.currentFrame.addData(this.bufferList)) { | ||
// Handle possible parsing errors | ||
if (this.currentFrame.protocolError) { | ||
// Something bad happened.. get rid of this client. | ||
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, this.currentFrame.dropReason); | ||
return; | ||
} | ||
else if (this.currentFrame.frameTooLarge) { | ||
this.drop(WebSocketConnection.CLOSE_REASON_MESSAGE_TOO_LARGE, this.currentFrame.dropReason); | ||
return; | ||
} | ||
if (!this.assembleFragments) { | ||
this.emit('frame', this.currentFrame); | ||
} | ||
this.processFrame(this.currentFrame); | ||
this.currentFrame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); | ||
// Handle possible parsing errors | ||
if (this.currentFrame.protocolError) { | ||
// Something bad happened.. get rid of this client. | ||
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, this.currentFrame.dropReason); | ||
return; | ||
} | ||
}, | ||
handleSocketError: function(error) { | ||
// console.log((new Date()) + " - Closing Connection: Socket Error: " + error); | ||
if (this.listeners('error').length > 0) { | ||
this.emit('error', error); | ||
else if (this.currentFrame.frameTooLarge) { | ||
this.drop(WebSocketConnection.CLOSE_REASON_MESSAGE_TOO_LARGE, this.currentFrame.dropReason); | ||
return; | ||
} | ||
this.socket.end(); | ||
}, | ||
if (!this.assembleFragments) { | ||
this.emit('frame', this.currentFrame); | ||
} | ||
this.processFrame(this.currentFrame); | ||
this.currentFrame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); | ||
} | ||
}; | ||
handleSocketEnd: function() { | ||
this.socket.end(); | ||
this.frameQueue = null; | ||
this.outgoingFrameQueue = null; | ||
this.fragmentationSize = 0; | ||
this.bufferList = null; | ||
}, | ||
WebSocketConnection.prototype.handleSocketError = function(error) { | ||
// console.log((new Date()) + " - Closing Connection: Socket Error: " + error); | ||
if (this.listeners('error').length > 0) { | ||
this.emit('error', error); | ||
} | ||
this.socket.end(); | ||
}; | ||
handleSocketClose: function(hadError) { | ||
this.socketHadError = hadError; | ||
WebSocketConnection.prototype.handleSocketEnd = function() { | ||
this.socket.end(); | ||
this.frameQueue = null; | ||
this.outgoingFrameQueue = null; | ||
this.fragmentationSize = 0; | ||
this.bufferList = null; | ||
}; | ||
WebSocketConnection.prototype.handleSocketClose = function(hadError) { | ||
this.socketHadError = hadError; | ||
this.connected = false; | ||
this.state = "closed"; | ||
if (!this.closeEventEmitted) { | ||
this.closeEventEmitted = true; | ||
this.emit('close', this); | ||
} | ||
if (this.config.keepalive) { | ||
clearInterval(this._pingIntervalID); | ||
} | ||
}; | ||
WebSocketConnection.prototype.handleSocketDrain = function() { | ||
this.outputPaused = false; | ||
this.processOutgoingFrameQueue(); | ||
}; | ||
WebSocketConnection.prototype.close = function() { | ||
if (this.connected) { | ||
this.setCloseTimer(); | ||
this.sendCloseFrame(); | ||
this.state = "closing"; | ||
this.connected = false; | ||
this.state = "closed"; | ||
if (!this.closeEventEmitted) { | ||
this.closeEventEmitted = true; | ||
this.emit('close', this); | ||
} | ||
if (this.config.keepalive) { | ||
clearInterval(this._pingIntervalID); | ||
} | ||
}, | ||
handleSocketDrain: function() { | ||
this.outputPaused = false; | ||
this.processOutgoingFrameQueue(); | ||
}, | ||
close: function() { | ||
if (this.connected) { | ||
this.setCloseTimer(); | ||
this.sendCloseFrame(); | ||
this.state = "closing"; | ||
this.connected = false; | ||
} | ||
}, | ||
drop: function(closeReason, reasonText) { | ||
if (typeof(closeReason) !== 'number') { | ||
closeReason = WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR; | ||
} | ||
var logText = "WebSocket: Dropping Connection. Code: " + closeReason.toString(10); | ||
if (reasonText) { | ||
logText += (" - " + reasonText); | ||
} | ||
console.error((new Date()) + " " + logText); | ||
this.outgoingFrameQueue = []; | ||
this.frameQueue = [] | ||
this.fragmentationSize = 0; | ||
this.sendCloseFrame(closeReason, reasonText, true); | ||
this.connected = false; | ||
this.state = "closed"; | ||
this.socket.destroy(); | ||
}, | ||
setCloseTimer: function() { | ||
this.clearCloseTimer(); | ||
this.waitingForCloseResponse = true; | ||
this.closeTimer = setTimeout(this._closeTimerHandler, this.closeTimeout); | ||
}, | ||
clearCloseTimer: function() { | ||
if (this.closeTimer) { | ||
clearTimeout(this.closeTimer); | ||
this.waitingForCloseResponse = false; | ||
this.closeTimer = null; | ||
} | ||
}, | ||
handleCloseTimer: function() { | ||
} | ||
}; | ||
WebSocketConnection.prototype.drop = function(closeReason, reasonText) { | ||
if (typeof(closeReason) !== 'number') { | ||
closeReason = WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR; | ||
} | ||
var logText = "WebSocket: Dropping Connection. Code: " + closeReason.toString(10); | ||
if (reasonText) { | ||
logText += (" - " + reasonText); | ||
} | ||
console.error((new Date()) + " " + logText); | ||
this.outgoingFrameQueue = []; | ||
this.frameQueue = [] | ||
this.fragmentationSize = 0; | ||
this.sendCloseFrame(closeReason, reasonText, true); | ||
this.connected = false; | ||
this.state = "closed"; | ||
this.socket.destroy(); | ||
}; | ||
WebSocketConnection.prototype.setCloseTimer = function() { | ||
this.clearCloseTimer(); | ||
this.waitingForCloseResponse = true; | ||
this.closeTimer = setTimeout(this._closeTimerHandler, this.closeTimeout); | ||
}; | ||
WebSocketConnection.prototype.clearCloseTimer = function() { | ||
if (this.closeTimer) { | ||
clearTimeout(this.closeTimer); | ||
this.waitingForCloseResponse = false; | ||
this.closeTimer = null; | ||
if (this.waitingForCloseResponse) { | ||
this.waitingForCloseResponse = false; | ||
this.socket.end(); | ||
} | ||
}, | ||
} | ||
} | ||
WebSocketConnection.prototype.handleCloseTimer = function() { | ||
this.closeTimer = null; | ||
if (this.waitingForCloseResponse) { | ||
this.waitingForCloseResponse = false; | ||
this.socket.end(); | ||
} | ||
}; | ||
WebSocketConnection.prototype.processFrame = function(frame) { | ||
var i; | ||
var message; | ||
processFrame: function(frame) { | ||
var i; | ||
var message; | ||
switch(frame.opcode) { | ||
case 0x02: // WebSocketFrame.BINARY_FRAME | ||
if (this.assembleFragments) { | ||
if (frame.fin) { | ||
// Complete single-frame message received | ||
this.emit('message', { | ||
type: 'binary', | ||
binaryData: frame.binaryPayload | ||
}); | ||
} | ||
else if (this.frameQueue.length === 0) { | ||
switch(frame.opcode) { | ||
case 0x02: // WebSocketFrame.BINARY_FRAME | ||
if (this.assembleFragments) { | ||
if (frame.fin) { | ||
// Complete single-frame message received | ||
this.emit('message', { | ||
type: 'binary', | ||
binaryData: frame.binaryPayload | ||
}); | ||
} | ||
else if (this.frameQueue.length === 0) { | ||
// beginning of a fragmented message | ||
this.frameQueue.push(frame); | ||
this.fragmentationOpcode = frame.opcode; | ||
this.fragmentationSize = frame.length; | ||
} | ||
else { | ||
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, | ||
"Illegal BINARY_FRAME received in the middle of a fragmented message. Expected a continuation or control frame."); | ||
return; | ||
} | ||
} | ||
break; | ||
case 0x01: // WebSocketFrame.TEXT_FRAME | ||
if (this.assembleFragments) { | ||
if (frame.fin) { | ||
// Complete single-frame message received | ||
this.emit('message', { | ||
type: 'utf8', | ||
utf8Data: frame.binaryPayload.toString('utf8') | ||
}); | ||
} | ||
else if (this.frameQueue.length === 0) { | ||
if (this.assembleFragments) { | ||
// beginning of a fragmented message | ||
@@ -212,258 +233,235 @@ this.frameQueue.push(frame); | ||
} | ||
else { | ||
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, | ||
"Illegal BINARY_FRAME received in the middle of a fragmented message. Expected a continuation or control frame."); | ||
return; | ||
} | ||
} | ||
break; | ||
case 0x01: // WebSocketFrame.TEXT_FRAME | ||
if (this.assembleFragments) { | ||
if (frame.fin) { | ||
// Complete single-frame message received | ||
this.emit('message', { | ||
type: 'utf8', | ||
utf8Data: frame.binaryPayload.toString('utf8') | ||
}); | ||
} | ||
else if (this.frameQueue.length === 0) { | ||
if (this.assembleFragments) { | ||
// beginning of a fragmented message | ||
this.frameQueue.push(frame); | ||
this.fragmentationOpcode = frame.opcode; | ||
this.fragmentationSize = frame.length; | ||
} | ||
} | ||
else { | ||
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, | ||
"Illegal TEXT_FRAME received in the middle of a fragmented message. Expected a continuation or control frame."); | ||
return; | ||
} | ||
else { | ||
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, | ||
"Illegal TEXT_FRAME received in the middle of a fragmented message. Expected a continuation or control frame."); | ||
return; | ||
} | ||
break; | ||
case 0x00: // WebSocketFrame.CONTINUATION | ||
if (this.assembleFragments) { | ||
if (this.fragmentationOpcode === 0x00 && frame.opcode === 0x00) { | ||
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, | ||
"Unexpected Continuation Frame"); | ||
return; | ||
} | ||
} | ||
break; | ||
case 0x00: // WebSocketFrame.CONTINUATION | ||
if (this.assembleFragments) { | ||
if (this.fragmentationOpcode === 0x00 && frame.opcode === 0x00) { | ||
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, | ||
"Unexpected Continuation Frame"); | ||
return; | ||
} | ||
this.fragmentationSize += frame.length; | ||
this.fragmentationSize += frame.length; | ||
if (this.fragmentationSize > this.maxReceivedMessageSize) { | ||
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, | ||
"Maximum message size exceeded."); | ||
return; | ||
} | ||
if (this.fragmentationSize > this.maxReceivedMessageSize) { | ||
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, | ||
"Maximum message size exceeded."); | ||
return; | ||
} | ||
this.frameQueue.push(frame); | ||
this.frameQueue.push(frame); | ||
if (frame.fin) { | ||
// end of fragmented message, so we process the whole | ||
// message now. We also have to decode the utf-8 data | ||
// for text frames after combining all the fragments. | ||
var bytesCopied = 0; | ||
var binaryPayload = new Buffer(this.fragmentationSize); | ||
this.frameQueue.forEach(function (currentFrame) { | ||
currentFrame.binaryPayload.copy(binaryPayload, bytesCopied); | ||
bytesCopied += currentFrame.binaryPayload.length; | ||
}); | ||
if (frame.fin) { | ||
// end of fragmented message, so we process the whole | ||
// message now. We also have to decode the utf-8 data | ||
// for text frames after combining all the fragments. | ||
var bytesCopied = 0; | ||
var binaryPayload = new Buffer(this.fragmentationSize); | ||
this.frameQueue.forEach(function (currentFrame) { | ||
currentFrame.binaryPayload.copy(binaryPayload, bytesCopied); | ||
bytesCopied += currentFrame.binaryPayload.length; | ||
}); | ||
switch (this.frameQueue[0].opcode) { | ||
case 0x02: // WebSocketOpcode.BINARY_FRAME | ||
this.emit('message', { | ||
type: 'binary', | ||
binaryData: binaryPayload | ||
}); | ||
break; | ||
case 0x01: // WebSocketOpcode.TEXT_FRAME | ||
this.emit('message', { | ||
type: 'utf8', | ||
utf8Data: binaryPayload.toString('utf8') | ||
}); | ||
break; | ||
default: | ||
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, | ||
"Unexpected first opcode in fragmentation sequence: 0x" + this.frameQueue[0].opcode.toString(16)); | ||
return; | ||
} | ||
this.frameQueue = []; | ||
this.fragmentationSize = 0; | ||
switch (this.frameQueue[0].opcode) { | ||
case 0x02: // WebSocketOpcode.BINARY_FRAME | ||
this.emit('message', { | ||
type: 'binary', | ||
binaryData: binaryPayload | ||
}); | ||
break; | ||
case 0x01: // WebSocketOpcode.TEXT_FRAME | ||
this.emit('message', { | ||
type: 'utf8', | ||
utf8Data: binaryPayload.toString('utf8') | ||
}); | ||
break; | ||
default: | ||
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, | ||
"Unexpected first opcode in fragmentation sequence: 0x" + this.frameQueue[0].opcode.toString(16)); | ||
return; | ||
} | ||
this.frameQueue = []; | ||
this.fragmentationSize = 0; | ||
} | ||
break; | ||
case 0x09: // WebSocketFrame.PING | ||
this.pong(frame.binaryPayload); | ||
break; | ||
case 0x0A: // WebSocketFrame.PONG | ||
break; | ||
case 0x08: // WebSocketFrame.CONNECTION_CLOSE | ||
if (this.waitingForCloseResponse) { | ||
// Got response to our request to close the connection. | ||
// Close is complete, so we just hang up. | ||
this.clearCloseTimer(); | ||
this.waitingForCloseResponse = false; | ||
this.state = "closed"; | ||
this.socket.end(); | ||
} | ||
else { | ||
// Got request from other party to close connection. | ||
// Send back acknowledgement and then hang up. | ||
this.state = "closing"; | ||
if (frame.closeStatus !== WebSocketConnection.CLOSE_REASON_NORMAL) { | ||
var logCloseError; | ||
switch(frame.closeStatus) { | ||
case WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR: | ||
logCloseError = "Remote peer closed connection: Protocol Error"; | ||
break; | ||
case WebSocketConnection.CLOSE_REASON_MESSAGE_TOO_LARGE: | ||
logCloseError = "Remote peer closed connection: Received Message Too Large"; | ||
break; | ||
case WebSocketConnection.CLOSE_REASON_UNPROCESSABLE_INPUT: | ||
logCloseError = "Remote peer closed connection: Unprocessable Input"; | ||
break; | ||
case WebSocketConnection.CLOSE_REASON_GOING_AWAY: | ||
logCloseError = "Remote peer closed connection: Going Away"; | ||
break; | ||
default: | ||
logCloseError = "Remote peer closed connection: Status code " + frame.closeStatus.toString(10); | ||
break; | ||
} | ||
if (frame.binaryPayload) { | ||
logCloseError += (" - Description Provided: " + frame.binaryPayload.toString('utf8')); | ||
} | ||
console.error((new Date()) + " " + logCloseError); | ||
} | ||
break; | ||
case 0x09: // WebSocketFrame.PING | ||
this.pong(frame.binaryPayload); | ||
break; | ||
case 0x0A: // WebSocketFrame.PONG | ||
break; | ||
case 0x08: // WebSocketFrame.CONNECTION_CLOSE | ||
if (this.waitingForCloseResponse) { | ||
// Got response to our request to close the connection. | ||
// Close is complete, so we just hang up. | ||
this.clearCloseTimer(); | ||
this.waitingForCloseResponse = false; | ||
this.state = "closed"; | ||
this.socket.end(); | ||
} | ||
else { | ||
// Got request from other party to close connection. | ||
// Send back acknowledgement and then hang up. | ||
this.state = "closing"; | ||
if (frame.closeStatus !== WebSocketConnection.CLOSE_REASON_NORMAL) { | ||
var logCloseError; | ||
switch(frame.closeStatus) { | ||
case WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR: | ||
logCloseError = "Remote peer closed connection: Protocol Error"; | ||
break; | ||
case WebSocketConnection.CLOSE_REASON_MESSAGE_TOO_LARGE: | ||
logCloseError = "Remote peer closed connection: Received Message Too Large"; | ||
break; | ||
case WebSocketConnection.CLOSE_REASON_UNPROCESSABLE_INPUT: | ||
logCloseError = "Remote peer closed connection: Unprocessable Input"; | ||
break; | ||
case WebSocketConnection.CLOSE_REASON_GOING_AWAY: | ||
logCloseError = "Remote peer closed connection: Going Away"; | ||
break; | ||
default: | ||
logCloseError = "Remote peer closed connection: Status code " + frame.closeStatus.toString(10); | ||
break; | ||
} | ||
else { | ||
console.log("Remote peer " + this.remoteAddress + " requested disconnect"); | ||
if (frame.binaryPayload) { | ||
logCloseError += (" - Description Provided: " + frame.binaryPayload.toString('utf8')); | ||
} | ||
this.sendCloseFrame(WebSocketConnection.CLOSE_REASON_NORMAL); | ||
this.socket.end(); | ||
console.error((new Date()) + " " + logCloseError); | ||
} | ||
break; | ||
default: | ||
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, | ||
"Unrecognized Opcode: 0x" + frame.opcode.toString(16)); | ||
break; | ||
} | ||
}, | ||
else { | ||
console.log((new Date()) + "Remote peer " + this.remoteAddress + " requested disconnect"); | ||
} | ||
this.sendCloseFrame(WebSocketConnection.CLOSE_REASON_NORMAL); | ||
this.socket.end(); | ||
} | ||
break; | ||
default: | ||
this.drop(WebSocketConnection.CLOSE_REASON_PROTOCOL_ERROR, | ||
"Unrecognized Opcode: 0x" + frame.opcode.toString(16)); | ||
break; | ||
} | ||
}; | ||
WebSocketConnection.prototype.sendUTF = function(data) { | ||
data = new Buffer(data); | ||
var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); | ||
frame.opcode = 0x01; // WebSocketOpcode.TEXT_FRAME | ||
frame.binaryPayload = new Buffer(data, 'utf8'); | ||
this.fragmentAndSend(frame); | ||
}; | ||
sendUTF: function(data) { | ||
data = new Buffer(data); | ||
var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); | ||
frame.opcode = 0x01; // WebSocketOpcode.TEXT_FRAME | ||
frame.binaryPayload = new Buffer(data, 'utf8'); | ||
this.fragmentAndSend(frame); | ||
}, | ||
WebSocketConnection.prototype.sendBytes = function(data) { | ||
var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); | ||
frame.opcode = 0x02; // WebSocketOpcode.BINARY_FRAME | ||
frame.binaryPayload = data; | ||
this.fragmentAndSend(frame); | ||
}; | ||
WebSocketConnection.prototype.ping = function() { | ||
var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); | ||
frame.opcode = 0x09; // WebSocketOpcode.PING | ||
frame.fin = true; | ||
this.sendFrame(frame); | ||
}; | ||
sendBytes: function(data) { | ||
var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); | ||
frame.opcode = 0x02; // WebSocketOpcode.BINARY_FRAME | ||
frame.binaryPayload = data; | ||
this.fragmentAndSend(frame); | ||
}, | ||
// Pong frames have to echo back the contents of the data portion of the | ||
// ping frame exactly, byte for byte. | ||
WebSocketConnection.prototype.pong = function(binaryPayload) { | ||
var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); | ||
frame.opcode = 0x0A; // WebSocketOpcode.PONG | ||
frame.binaryPayload = binaryPayload; | ||
frame.fin = true; | ||
this.sendFrame(frame); | ||
}; | ||
WebSocketConnection.prototype.fragmentAndSend = function(frame) { | ||
if (frame.opcode > 0x07) { | ||
throw new Error("You cannot fragment control frames."); | ||
} | ||
ping: function() { | ||
var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); | ||
frame.opcode = 0x09; // WebSocketOpcode.PING | ||
frame.fin = true; | ||
this.sendFrame(frame); | ||
}, | ||
var threshold = this.config.fragmentationThreshold; | ||
var length = frame.binaryPayload.length; | ||
// Pong frames have to echo back the contents of the data portion of the | ||
// ping frame exactly, byte for byte. | ||
pong: function(binaryPayload) { | ||
var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); | ||
frame.opcode = 0x0A; // WebSocketOpcode.PONG | ||
frame.binaryPayload = binaryPayload; | ||
if (this.config.fragmentOutgoingMessages && frame.binaryPayload && length > threshold) { | ||
var numFragments = Math.ceil(length / threshold); | ||
for (var i=1; i <= numFragments; i++) { | ||
var currentFrame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); | ||
// continuation opcode except for first frame. | ||
currentFrame.opcode = (i === 1) ? frame.opcode : 0x00; | ||
// fin set on last frame only | ||
currentFrame.fin = (i === numFragments); | ||
// length is likely to be shorter on the last fragment | ||
var currentLength = (i === numFragments) ? length - (threshold * (i-1)) : threshold; | ||
var sliceStart = threshold * (i-1); | ||
// Slice the right portion of the original payload | ||
currentFrame.binaryPayload = frame.binaryPayload.slice(sliceStart, sliceStart + currentLength); | ||
this.sendFrame(currentFrame); | ||
} | ||
} | ||
else { | ||
frame.fin = true; | ||
this.sendFrame(frame); | ||
}, | ||
} | ||
}; | ||
WebSocketConnection.prototype.sendCloseFrame = function(reasonCode, reasonText, force) { | ||
var reasonLength = 0; | ||
if (typeof(reasonCode) !== 'number') { | ||
reasonCode = WebSocketConnection.CLOSE_REASON_NORMAL; | ||
} | ||
if (typeof(reasonText) === 'string') { | ||
reasonLength = Buffer.byteLength(reasonText, 'utf8'); | ||
} | ||
var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); | ||
frame.fin = true; | ||
frame.opcode = 0x08; // WebSocketOpcode.CONNECTION_CLOSE | ||
frame.closeStatus = reasonCode; | ||
if (reasonText) { | ||
frame.binaryPayload = new Buffer(reasonText, 'utf8'); | ||
} | ||
fragmentAndSend: function(frame) { | ||
if (frame.opcode > 0x07) { | ||
throw new Error("You cannot fragment control frames."); | ||
} | ||
var threshold = this.config.fragmentationThreshold; | ||
var length = frame.binaryPayload.length; | ||
if (this.config.fragmentOutgoingMessages && frame.binaryPayload && length > threshold) { | ||
var numFragments = Math.ceil(length / threshold); | ||
for (var i=1; i <= numFragments; i++) { | ||
var currentFrame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); | ||
// continuation opcode except for first frame. | ||
currentFrame.opcode = (i === 1) ? frame.opcode : 0x00; | ||
// fin set on last frame only | ||
currentFrame.fin = (i === numFragments); | ||
// length is likely to be shorter on the last fragment | ||
var currentLength = (i === numFragments) ? length - (threshold * (i-1)) : threshold; | ||
var sliceStart = threshold * (i-1); | ||
// Slice the right portion of the original payload | ||
currentFrame.binaryPayload = frame.binaryPayload.slice(sliceStart, sliceStart + currentLength); | ||
this.sendFrame(currentFrame); | ||
} | ||
} | ||
else { | ||
frame.fin = true; | ||
this.sendFrame(frame); | ||
} | ||
}, | ||
sendCloseFrame: function(reasonCode, reasonText, force) { | ||
var reasonLength = 0; | ||
if (typeof(reasonCode) !== 'number') { | ||
reasonCode = WebSocketConnection.CLOSE_REASON_NORMAL; | ||
} | ||
if (typeof(reasonText) === 'string') { | ||
reasonLength = Buffer.byteLength(reasonText, 'utf8'); | ||
} | ||
var frame = new WebSocketFrame(this.maskBytes, this.frameHeader, this.config); | ||
frame.fin = true; | ||
frame.opcode = 0x08; // WebSocketOpcode.CONNECTION_CLOSE | ||
frame.closeStatus = reasonCode; | ||
if (reasonText) { | ||
frame.binaryPayload = new Buffer(reasonText, 'utf8'); | ||
} | ||
this.sendFrame(frame, force); | ||
}, | ||
sendFrame: function(frame, force) { | ||
frame.mask = this.maskOutgoingPackets; | ||
var buffer = frame.toBuffer(); | ||
this.outgoingFrameQueue.unshift(buffer); | ||
this.bytesWaitingToFlush += buffer.length; | ||
if (!this.outputPaused || force) { | ||
this.processOutgoingFrameQueue(); | ||
} | ||
}, | ||
processOutgoingFrameQueue: function() { | ||
if (this.outputPaused) { return; } | ||
this.sendFrame(frame, force); | ||
}; | ||
WebSocketConnection.prototype.sendFrame = function(frame, force) { | ||
frame.mask = this.maskOutgoingPackets; | ||
var buffer = frame.toBuffer(); | ||
this.outgoingFrameQueue.unshift(buffer); | ||
this.bytesWaitingToFlush += buffer.length; | ||
if (!this.outputPaused || force) { | ||
this.processOutgoingFrameQueue(); | ||
} | ||
}; | ||
WebSocketConnection.prototype.processOutgoingFrameQueue = function() { | ||
if (this.outputPaused) { return; } | ||
if (this.outgoingFrameQueue.length > 0) { | ||
var buffer = this.outgoingFrameQueue.pop(); | ||
try { | ||
if (this.outgoingFrameQueue.length > 0) { | ||
var buffer = this.outgoingFrameQueue.pop(); | ||
var flushed = this.socket.write(buffer); | ||
this.bytesWaitingToFlush -= buffer.length; | ||
if (!flushed) { | ||
this.outputPaused = true; | ||
return; | ||
} | ||
process.nextTick(this.outgoingFrameQueueHandler); | ||
} | ||
var flushed = this.socket.write(buffer); | ||
} | ||
catch(e) { | ||
console.error("Error while writing to socket: " + e.toString()); | ||
return; | ||
} | ||
this.bytesWaitingToFlush -= buffer.length; | ||
if (!flushed) { | ||
this.outputPaused = true; | ||
return; | ||
} | ||
process.nextTick(this.outgoingFrameQueueHandler); | ||
} | ||
}); | ||
}; | ||
module.exports = WebSocketConnection; |
@@ -1,2 +0,1 @@ | ||
var extend = require('./utils').extend; | ||
var ctio = require('../vendor/node-ctype/ctio-faster'); | ||
@@ -25,229 +24,230 @@ | ||
extend(WebSocketFrame.prototype, { | ||
addData: function(bufferList, fragmentationType) { | ||
var temp; | ||
if (this.parseState === DECODE_HEADER) { | ||
if (bufferList.length >= 2) { | ||
bufferList.joinInto(this.frameHeader, 0, 0, 2); | ||
bufferList.advance(2); | ||
var firstByte = this.frameHeader[0]; | ||
var secondByte = this.frameHeader[1]; | ||
WebSocketFrame.prototype.addData = function(bufferList, fragmentationType) { | ||
var temp; | ||
if (this.parseState === DECODE_HEADER) { | ||
if (bufferList.length >= 2) { | ||
bufferList.joinInto(this.frameHeader, 0, 0, 2); | ||
bufferList.advance(2); | ||
var firstByte = this.frameHeader[0]; | ||
var secondByte = this.frameHeader[1]; | ||
this.fin = Boolean(firstByte & 0x80); | ||
this.rsv1 = Boolean(firstByte & 0x40); | ||
this.rsv2 = Boolean(firstByte & 0x20); | ||
this.rsv3 = Boolean(firstByte & 0x10); | ||
this.mask = Boolean(secondByte & 0x80); | ||
this.fin = Boolean(firstByte & 0x80); | ||
this.rsv1 = Boolean(firstByte & 0x40); | ||
this.rsv2 = Boolean(firstByte & 0x20); | ||
this.rsv3 = Boolean(firstByte & 0x10); | ||
this.mask = Boolean(secondByte & 0x80); | ||
this.opcode = firstByte & 0x0F; | ||
this.length = secondByte & 0x7F; | ||
if (this.length === 126) { | ||
this.parseState = WAITING_FOR_16_BIT_LENGTH; | ||
} | ||
else if (this.length === 127) { | ||
this.parseState = WAITING_FOR_64_BIT_LENGTH; | ||
} | ||
else { | ||
this.parseState = WAITING_FOR_MASK_KEY; | ||
} | ||
this.opcode = firstByte & 0x0F; | ||
this.length = secondByte & 0x7F; | ||
if (this.length === 126) { | ||
this.parseState = WAITING_FOR_16_BIT_LENGTH; | ||
} | ||
} | ||
if (this.parseState === WAITING_FOR_16_BIT_LENGTH) { | ||
if (bufferList.length >= 2) { | ||
bufferList.joinInto(this.frameHeader, 2, 0, 2); | ||
bufferList.advance(2); | ||
this.length = ctio.ruint16(this.frameHeader, 'big', 2); | ||
else if (this.length === 127) { | ||
this.parseState = WAITING_FOR_64_BIT_LENGTH; | ||
} | ||
else { | ||
this.parseState = WAITING_FOR_MASK_KEY; | ||
} | ||
} | ||
else if (this.parseState === WAITING_FOR_64_BIT_LENGTH) { | ||
if (bufferList.length >= 8) { | ||
bufferList.joinInto(this.frameHeader, 2, 0, 8); | ||
bufferList.advance(8); | ||
var lengthPair = ctio.ruint64(this.frameHeader, 'big', 2); | ||
if (lengthPair[0] !== 0) { | ||
this.protocolError = true; | ||
this.dropReason = "Unsupported 64-bit length frame received"; | ||
return true; | ||
} | ||
this.length = lengthPair[1]; | ||
this.parseState = WAITING_FOR_MASK_KEY; | ||
} | ||
if (this.parseState === WAITING_FOR_16_BIT_LENGTH) { | ||
if (bufferList.length >= 2) { | ||
bufferList.joinInto(this.frameHeader, 2, 0, 2); | ||
bufferList.advance(2); | ||
this.length = ctio.ruint16(this.frameHeader, 'big', 2); | ||
this.parseState = WAITING_FOR_MASK_KEY; | ||
} | ||
} | ||
else if (this.parseState === WAITING_FOR_64_BIT_LENGTH) { | ||
if (bufferList.length >= 8) { | ||
bufferList.joinInto(this.frameHeader, 2, 0, 8); | ||
bufferList.advance(8); | ||
var lengthPair = ctio.ruint64(this.frameHeader, 'big', 2); | ||
if (lengthPair[0] !== 0) { | ||
this.protocolError = true; | ||
this.dropReason = "Unsupported 64-bit length frame received"; | ||
return true; | ||
} | ||
this.length = lengthPair[1]; | ||
this.parseState = WAITING_FOR_MASK_KEY; | ||
} | ||
if (this.parseState === WAITING_FOR_MASK_KEY) { | ||
if (this.mask) { | ||
if (bufferList.length >= 4) { | ||
bufferList.joinInto(this.maskBytes, 0, 0, 4); | ||
bufferList.advance(4); | ||
this.maskPos = 0; | ||
this.parseState = WAITING_FOR_PAYLOAD; | ||
} | ||
} | ||
else { | ||
} | ||
if (this.parseState === WAITING_FOR_MASK_KEY) { | ||
if (this.mask) { | ||
if (bufferList.length >= 4) { | ||
bufferList.joinInto(this.maskBytes, 0, 0, 4); | ||
bufferList.advance(4); | ||
this.maskPos = 0; | ||
this.parseState = WAITING_FOR_PAYLOAD; | ||
} | ||
} | ||
else { | ||
this.parseState = WAITING_FOR_PAYLOAD; | ||
} | ||
} | ||
if (this.parseState === WAITING_FOR_PAYLOAD) { | ||
if (this.length > this.maxReceivedFrameSize) { | ||
this.frameTooLarge = true; | ||
this.dropReason = "Frame size of " + this.length.toString(10) + | ||
" bytes exceeds maximum accepted frame size"; | ||
return true; | ||
} | ||
if (this.parseState === WAITING_FOR_PAYLOAD) { | ||
if (this.length > this.maxReceivedFrameSize) { | ||
this.frameTooLarge = true; | ||
this.dropReason = "Frame size of " + this.length.toString(10) + | ||
" bytes exceeds maximum accepted frame size"; | ||
return true; | ||
if (this.length === 0) { | ||
this.binaryPayload = new Buffer(0); | ||
this.parseState = COMPLETE; | ||
return true; | ||
} | ||
if (bufferList.length >= this.length) { | ||
this.binaryPayload = bufferList.take(this.length); | ||
bufferList.advance(this.length); | ||
if (this.mask) { | ||
this.applyMask(this.binaryPayload, 0, this.length); | ||
} | ||
if (this.length === 0) { | ||
this.binaryPayload = new Buffer(0); | ||
this.parseState = COMPLETE; | ||
return true; | ||
if (this.opcode === 0x08) { // WebSocketOpcode.CONNECTION_CLOSE | ||
this.closeStatus = ctio.ruint16(this.binaryPayload, 'big', 0); | ||
this.binaryPayload = this.binaryPayload.slice(2); | ||
} | ||
if (bufferList.length >= this.length) { | ||
this.binaryPayload = bufferList.take(this.length); | ||
bufferList.advance(this.length); | ||
if (this.mask) { | ||
this.applyMask(this.binaryPayload, 0, this.length); | ||
} | ||
if (this.opcode === 0x08) { // WebSocketOpcode.CONNECTION_CLOSE | ||
this.closeStatus = ctio.ruint16(this.binaryPayload, 'big', 0); | ||
this.binaryPayload = this.binaryPayload.slice(2); | ||
} | ||
this.parseState = COMPLETE; | ||
return true; | ||
} | ||
} | ||
return false; | ||
}, | ||
throwAwayPayload: function(bufferList) { | ||
if (bufferList.length >= this.length) { | ||
bufferList.advance(this.length); | ||
this.parseState = COMPLETE; | ||
return true; | ||
} | ||
return false; | ||
}, | ||
applyMask: function(buffer, offset, length) { | ||
var end = offset + length; | ||
for (var i=offset; i < end; i++) { | ||
buffer[i] = buffer[i] ^ this.maskBytes[this.maskPos]; | ||
this.maskPos = (this.maskPos + 1) & 3; | ||
} | ||
}, | ||
toBuffer: function(nullMask) { | ||
var maskKey; | ||
var headerLength = 2; | ||
var data; | ||
var outputPos; | ||
var firstByte = 0x00; | ||
var secondByte = 0x00; | ||
if (this.fin) { | ||
firstByte |= 0x80; | ||
} | ||
if (this.rsv1) { | ||
firstByte |= 0x40; | ||
} | ||
if (this.rsv2) { | ||
firstByte |= 0x20; | ||
} | ||
if (this.rsv3) { | ||
firstByte |= 0x10; | ||
} | ||
if (this.mask) { | ||
secondByte |= 0x80; | ||
} | ||
} | ||
return false; | ||
}; | ||
firstByte |= (this.opcode & 0x0F); | ||
WebSocketFrame.prototype.throwAwayPayload = function(bufferList) { | ||
if (bufferList.length >= this.length) { | ||
bufferList.advance(this.length); | ||
this.parseState = COMPLETE; | ||
return true; | ||
} | ||
return false; | ||
}; | ||
// the close frame is a special case because the close reason is | ||
// prepended to the payload data. | ||
if (this.opcode === 0x08) { | ||
this.length = 2; | ||
if (this.binaryPayload) { | ||
this.length += this.binaryPayload.length; | ||
} | ||
data = new Buffer(this.length); | ||
ctio.wuint16(this.closeStatus, 'big', data, 0); | ||
if (this.length > 2) { | ||
this.binaryPayload.copy(data, 2); | ||
} | ||
} | ||
else if (this.binaryPayload) { | ||
data = this.binaryPayload; | ||
this.length = data.length; | ||
} | ||
else { | ||
this.length = 0; | ||
} | ||
WebSocketFrame.prototype.applyMask = function(buffer, offset, length) { | ||
var end = offset + length; | ||
for (var i=offset; i < end; i++) { | ||
buffer[i] = buffer[i] ^ this.maskBytes[this.maskPos]; | ||
this.maskPos = (this.maskPos + 1) & 3; | ||
} | ||
}; | ||
if (this.length <= 125) { | ||
// encode the length directly into the two-byte frame header | ||
secondByte |= (this.length & 0x7F); | ||
WebSocketFrame.prototype.toBuffer = function(nullMask) { | ||
var maskKey; | ||
var headerLength = 2; | ||
var data; | ||
var outputPos; | ||
var firstByte = 0x00; | ||
var secondByte = 0x00; | ||
if (this.fin) { | ||
firstByte |= 0x80; | ||
} | ||
if (this.rsv1) { | ||
firstByte |= 0x40; | ||
} | ||
if (this.rsv2) { | ||
firstByte |= 0x20; | ||
} | ||
if (this.rsv3) { | ||
firstByte |= 0x10; | ||
} | ||
if (this.mask) { | ||
secondByte |= 0x80; | ||
} | ||
firstByte |= (this.opcode & 0x0F); | ||
// the close frame is a special case because the close reason is | ||
// prepended to the payload data. | ||
if (this.opcode === 0x08) { | ||
this.length = 2; | ||
if (this.binaryPayload) { | ||
this.length += this.binaryPayload.length; | ||
} | ||
else if (this.length > 125 && this.length <= 0xFFFF) { | ||
// Use 16-bit length | ||
secondByte |= 126; | ||
headerLength += 2; | ||
data = new Buffer(this.length); | ||
ctio.wuint16(this.closeStatus, 'big', data, 0); | ||
if (this.length > 2) { | ||
this.binaryPayload.copy(data, 2); | ||
} | ||
else if (this.length > 0xFFFF) { | ||
// Use 64-bit length | ||
secondByte |= 127; | ||
headerLength += 8; | ||
} | ||
} | ||
else if (this.binaryPayload) { | ||
data = this.binaryPayload; | ||
this.length = data.length; | ||
} | ||
else { | ||
this.length = 0; | ||
} | ||
output = new Buffer(this.length + headerLength + (this.mask ? 4 : 0)); | ||
if (this.length <= 125) { | ||
// encode the length directly into the two-byte frame header | ||
secondByte |= (this.length & 0x7F); | ||
} | ||
else if (this.length > 125 && this.length <= 0xFFFF) { | ||
// Use 16-bit length | ||
secondByte |= 126; | ||
headerLength += 2; | ||
} | ||
else if (this.length > 0xFFFF) { | ||
// Use 64-bit length | ||
secondByte |= 127; | ||
headerLength += 8; | ||
} | ||
// write the frame header | ||
output[0] = firstByte; | ||
output[1] = secondByte; | ||
output = new Buffer(this.length + headerLength + (this.mask ? 4 : 0)); | ||
outputPos = 2; | ||
if (this.length > 125 && this.length <= 0xFFFF) { | ||
// write 16-bit length | ||
ctio.wuint16(this.length, 'big', output, outputPos); | ||
outputPos += 2; | ||
} | ||
else if (this.length > 0xFFFF) { | ||
// write 64-bit length | ||
ctio.wuint64([0x00000000, this.length], 'big', output, outputPos); | ||
outputPos += 8; | ||
} | ||
if (this.length > 0) { | ||
if (this.mask) { | ||
if (!nullMask) { | ||
// Generate a mask key | ||
maskKey = parseInt(Math.random()*0xFFFFFFFF); | ||
} | ||
else { | ||
maskKey = 0x00000000; | ||
} | ||
ctio.wuint32(maskKey, 'big', this.maskBytes, 0); | ||
this.maskPos = 0; | ||
// write the frame header | ||
output[0] = firstByte; | ||
output[1] = secondByte; | ||
// write the mask key | ||
this.maskBytes.copy(output, outputPos); | ||
outputPos += 4; | ||
data.copy(output, outputPos); | ||
this.applyMask(output, outputPos, data.length); | ||
outputPos = 2; | ||
if (this.length > 125 && this.length <= 0xFFFF) { | ||
// write 16-bit length | ||
ctio.wuint16(this.length, 'big', output, outputPos); | ||
outputPos += 2; | ||
} | ||
else if (this.length > 0xFFFF) { | ||
// write 64-bit length | ||
ctio.wuint64([0x00000000, this.length], 'big', output, outputPos); | ||
outputPos += 8; | ||
} | ||
if (this.length > 0) { | ||
if (this.mask) { | ||
if (!nullMask) { | ||
// Generate a mask key | ||
maskKey = parseInt(Math.random()*0xFFFFFFFF); | ||
} | ||
else { | ||
data.copy(output, outputPos); | ||
maskKey = 0x00000000; | ||
} | ||
ctio.wuint32(maskKey, 'big', this.maskBytes, 0); | ||
this.maskPos = 0; | ||
// write the mask key | ||
this.maskBytes.copy(output, outputPos); | ||
outputPos += 4; | ||
data.copy(output, outputPos); | ||
this.applyMask(output, outputPos, data.length); | ||
} | ||
return output; | ||
}, | ||
else { | ||
data.copy(output, outputPos); | ||
} | ||
} | ||
toString: function() { | ||
return "Opcode: " + this.opcode + ", fin: " + this.fin + ", length: " + this.length + ", hasPayload: " + Boolean(this.binaryPayload) + ", masked: " + this.mask; | ||
} | ||
}); | ||
return output; | ||
}; | ||
WebSocketFrame.prototype.toString = function() { | ||
return "Opcode: " + this.opcode + ", fin: " + this.fin + ", length: " + this.length + ", hasPayload: " + Boolean(this.binaryPayload) + ", masked: " + this.mask; | ||
}; | ||
module.exports = WebSocketFrame; |
@@ -1,2 +0,1 @@ | ||
var extend = require('./utils').extend; | ||
var crypto = require('crypto'); | ||
@@ -68,145 +67,145 @@ var util = require('util'); | ||
extend(WebSocketRequest.prototype, { | ||
readHandshake: function(request) { | ||
if (!request.headers['host']) { | ||
throw new Error("Client must provide a Host header."); | ||
} | ||
this.key = request.headers['sec-websocket-key']; | ||
if (!this.key) { | ||
this.reject(400, "Client must provide a value for Sec-WebSocket-Key."); | ||
throw new Error("Client must provide a value for Sec-WebSocket-Key."); | ||
} | ||
this.origin = request.headers['sec-websocket-origin']; | ||
this.websocketVersion = request.headers['sec-websocket-version']; | ||
if (!this.websocketVersion) { | ||
this.reject(400, "Client must provide a value for Sec-WebSocket-Version."); | ||
throw new Error("Client must provide a value for Sec-WebSocket-Version."); | ||
} | ||
if (this.websocketVersion !== '8') { | ||
this.reject(426, "Unsupported websocket client version.", { | ||
"Sec-WebSocket-Version": "8" | ||
}); | ||
throw new Error("Unsupported websocket client version. Client requested version " + this.websocketVersion + ", but we only support version 8."); | ||
} | ||
// Protocol is optional. | ||
var protocolString = request.headers['sec-websocket-protocol']; | ||
if (protocolString) { | ||
this.requestedProtocols = protocolString.toLocaleLowerCase().split(headerValueSplitRegExp); | ||
} | ||
else { | ||
this.requestedProtocols = []; | ||
} | ||
if (request.headers['x-forwarded-for']) { | ||
this.remoteAddress = request.headers['x-forwarded-for'].split(', ')[0]; | ||
} | ||
// Extensions are optional. | ||
var extensionsString = request.headers['sec-websocket-extensions']; | ||
this.requestedExtensions = this.parseExtensions(extensionsString); | ||
}, | ||
parseExtensions: function(extensionsString) { | ||
if (!extensionsString || extensionsString.length === 0) { | ||
return []; | ||
} | ||
extensions = extensionsString.toLocaleLowerCase().split(headerValueSplitRegExp); | ||
extensions.forEach(function(extension, index, array) { | ||
var params = extension.split(headerParamSplitRegExp); | ||
var extensionName = params[0]; | ||
var extensionParams = params.slice(1); | ||
extensionParams.forEach(function(rawParam, index, array) { | ||
var arr = rawParam.split('='); | ||
var obj = { | ||
name: arr[0], | ||
value: arr[1] | ||
}; | ||
array.splice(index, 1, obj); | ||
}); | ||
WebSocketRequest.prototype.readHandshake = function(request) { | ||
if (!request.headers['host']) { | ||
throw new Error("Client must provide a Host header."); | ||
} | ||
this.key = request.headers['sec-websocket-key']; | ||
if (!this.key) { | ||
this.reject(400, "Client must provide a value for Sec-WebSocket-Key."); | ||
throw new Error("Client must provide a value for Sec-WebSocket-Key."); | ||
} | ||
this.origin = request.headers['sec-websocket-origin']; | ||
this.websocketVersion = request.headers['sec-websocket-version']; | ||
if (!this.websocketVersion) { | ||
this.reject(400, "Client must provide a value for Sec-WebSocket-Version."); | ||
throw new Error("Client must provide a value for Sec-WebSocket-Version."); | ||
} | ||
if (this.websocketVersion !== '8') { | ||
this.reject(426, "Unsupported websocket client version.", { | ||
"Sec-WebSocket-Version": "8" | ||
}); | ||
throw new Error("Unsupported websocket client version. Client requested version " + this.websocketVersion + ", but we only support version 8."); | ||
} | ||
// Protocol is optional. | ||
var protocolString = request.headers['sec-websocket-protocol']; | ||
if (protocolString) { | ||
this.requestedProtocols = protocolString.toLocaleLowerCase().split(headerValueSplitRegExp); | ||
} | ||
else { | ||
this.requestedProtocols = []; | ||
} | ||
if (request.headers['x-forwarded-for']) { | ||
this.remoteAddress = request.headers['x-forwarded-for'].split(', ')[0]; | ||
} | ||
// Extensions are optional. | ||
var extensionsString = request.headers['sec-websocket-extensions']; | ||
this.requestedExtensions = this.parseExtensions(extensionsString); | ||
}; | ||
WebSocketRequest.prototype.parseExtensions = function(extensionsString) { | ||
if (!extensionsString || extensionsString.length === 0) { | ||
return []; | ||
} | ||
extensions = extensionsString.toLocaleLowerCase().split(headerValueSplitRegExp); | ||
extensions.forEach(function(extension, index, array) { | ||
var params = extension.split(headerParamSplitRegExp); | ||
var extensionName = params[0]; | ||
var extensionParams = params.slice(1); | ||
extensionParams.forEach(function(rawParam, index, array) { | ||
var arr = rawParam.split('='); | ||
var obj = { | ||
name: extensionName, | ||
params: extensionParams | ||
name: arr[0], | ||
value: arr[1] | ||
}; | ||
array.splice(index, 1, obj); | ||
}); | ||
return extensions; | ||
}, | ||
accept: function(acceptedProtocol, allowedOrigin) { | ||
// TODO: Handle extensions | ||
var connection = new WebSocketConnection(this.socket, [], acceptedProtocol, false, this.serverConfig); | ||
connection.remoteAddress = this.remoteAddress; | ||
// Create key validation hash | ||
var sha1 = crypto.createHash('sha1'); | ||
sha1.update(this.key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); | ||
var acceptKey = sha1.digest('base64'); | ||
var response = "HTTP/1.1 101 Switching Protocols\r\n" + | ||
"Upgrade: websocket\r\n" + | ||
"Connection: Upgrade\r\n" + | ||
"Sec-WebSocket-Accept: " + acceptKey + "\r\n"; | ||
if (acceptedProtocol) { | ||
// validate protocol | ||
for (var i=0; i < acceptedProtocol.length; i++) { | ||
var charCode = acceptedProtocol.charCodeAt(i); | ||
var character = acceptedProtocol.charAt(i); | ||
if (charCode < 0x21 || charCode > 0x7E || protocolSeparators.indexOf(character) !== -1) { | ||
this.reject(500); | ||
throw new Error("Illegal character '" + String.fromCharCode(character) + "' in subprotocol."); | ||
} | ||
} | ||
if (this.requestedProtocols.indexOf(acceptedProtocol) === -1) { | ||
var obj = { | ||
name: extensionName, | ||
params: extensionParams | ||
}; | ||
array.splice(index, 1, obj); | ||
}); | ||
return extensions; | ||
}, | ||
WebSocketRequest.prototype.accept = function(acceptedProtocol, allowedOrigin) { | ||
// TODO: Handle extensions | ||
var connection = new WebSocketConnection(this.socket, [], acceptedProtocol, false, this.serverConfig); | ||
connection.remoteAddress = this.remoteAddress; | ||
// Create key validation hash | ||
var sha1 = crypto.createHash('sha1'); | ||
sha1.update(this.key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); | ||
var acceptKey = sha1.digest('base64'); | ||
var response = "HTTP/1.1 101 Switching Protocols\r\n" + | ||
"Upgrade: websocket\r\n" + | ||
"Connection: Upgrade\r\n" + | ||
"Sec-WebSocket-Accept: " + acceptKey + "\r\n"; | ||
if (acceptedProtocol) { | ||
// validate protocol | ||
for (var i=0; i < acceptedProtocol.length; i++) { | ||
var charCode = acceptedProtocol.charCodeAt(i); | ||
var character = acceptedProtocol.charAt(i); | ||
if (charCode < 0x21 || charCode > 0x7E || protocolSeparators.indexOf(character) !== -1) { | ||
this.reject(500); | ||
throw new Error("Specified protocol was not requested by the client."); | ||
throw new Error("Illegal character '" + String.fromCharCode(character) + "' in subprotocol."); | ||
} | ||
acceptedProtocol = acceptedProtocol.replace(headerSanitizeRegExp, ''); | ||
response += "Sec-WebSocket-Protocol: " + acceptedProtocol + "\r\n"; | ||
} | ||
if (this.requestedProtocols.indexOf(acceptedProtocol) === -1) { | ||
this.reject(500); | ||
throw new Error("Specified protocol was not requested by the client."); | ||
} | ||
acceptedProtocol = acceptedProtocol.replace(headerSanitizeRegExp, ''); | ||
response += "Sec-WebSocket-Protocol: " + acceptedProtocol + "\r\n"; | ||
} | ||
if (allowedOrigin) { | ||
allowedOrigin = allowedOrigin.replace(headerSanitizeRegExp, ''); | ||
response += "Sec-WebSocket-Origin: " + allowedOrigin + "\r\n"; | ||
} | ||
// TODO: handle negotiated extensions | ||
// if (negotiatedExtensions) { | ||
// response += "Sec-WebSocket-Extensions: " + negotiatedExtensions.join(", ") + "\r\n"; | ||
// } | ||
response += "\r\n"; | ||
this.socket.write(response, 'ascii'); | ||
this.emit('requestAccepted', connection); | ||
return connection; | ||
}; | ||
WebSocketRequest.prototype.reject = function(status, reason, extraHeaders) { | ||
if (typeof(status) !== 'number') { | ||
status = 403; | ||
} | ||
var response = "HTTP/1.1 " + status + " " + httpStatusDescriptions[status] + "\r\n" + | ||
"Connection: close\r\n"; | ||
if (reason) { | ||
reason = reason.replace(headerSanitizeRegExp, ''); | ||
response += "X-WebSocket-Reject-Reason: " + reason + "\r\n"; | ||
} | ||
if (extraHeaders) { | ||
for (var key in extraHeaders) { | ||
var sanitizedValue = extraHeaders[key].toString().replace(headerSanitizeRegExp, ''); | ||
var sanitizedKey = key.replace(headerSanitizeRegExp, ''); | ||
response += (key + ": " + sanitizedValue + "\r\n"); | ||
} | ||
if (allowedOrigin) { | ||
allowedOrigin = allowedOrigin.replace(headerSanitizeRegExp, ''); | ||
response += "Sec-WebSocket-Origin: " + allowedOrigin + "\r\n"; | ||
} | ||
// TODO: handle negotiated extensions | ||
// if (negotiatedExtensions) { | ||
// response += "Sec-WebSocket-Extensions: " + negotiatedExtensions.join(", ") + "\r\n"; | ||
// } | ||
response += "\r\n"; | ||
this.socket.write(response, 'ascii'); | ||
this.emit('requestAccepted', connection); | ||
return connection; | ||
}, | ||
reject: function(status, reason, extraHeaders) { | ||
if (typeof(status) !== 'number') { | ||
status = 403; | ||
} | ||
var response = "HTTP/1.1 " + status + " " + httpStatusDescriptions[status] + "\r\n" + | ||
"Connection: close\r\n"; | ||
if (reason) { | ||
reason = reason.replace(headerSanitizeRegExp, ''); | ||
response += "X-WebSocket-Reject-Reason: " + reason + "\r\n"; | ||
} | ||
if (extraHeaders) { | ||
for (var key in extraHeaders) { | ||
var sanitizedValue = extraHeaders[key].toString().replace(headerSanitizeRegExp, ''); | ||
var sanitizedKey = key.replace(headerSanitizeRegExp, ''); | ||
response += (key + ": " + sanitizedValue + "\r\n"); | ||
} | ||
} | ||
response += "\r\n"; | ||
this.socket.end(response, 'ascii'); | ||
this.emit('requestRejected', this); | ||
} | ||
}); | ||
response += "\r\n"; | ||
this.socket.end(response, 'ascii'); | ||
this.emit('requestRejected', this); | ||
}; | ||
module.exports = WebSocketRequest; |
@@ -24,112 +24,112 @@ var extend = require('./utils').extend; | ||
extend(WebSocketRouter.prototype, { | ||
attachServer: function(server) { | ||
if (server) { | ||
this.server = server; | ||
this.server.on('request', this._requestHandler); | ||
} | ||
else { | ||
throw new Error("You must specify a WebSocketServer instance to attach to."); | ||
} | ||
}, | ||
detachServer: function() { | ||
if (this.server) { | ||
this.server.removeListener('request', this._requestHandler); | ||
this.server = null; | ||
} | ||
else { | ||
throw new Error("Cannot detach from server: not attached.") | ||
} | ||
}, | ||
mount: function(path, protocol, callback) { | ||
if (!path) { | ||
throw new Error("You must specify a path for this handler."); | ||
} | ||
if (!protocol) { | ||
protocol = "____no_protocol____"; | ||
} | ||
if (!callback) { | ||
throw new Error("You must specify a callback for this handler."); | ||
} | ||
WebSocketRouter.prototype.attachServer = function(server) { | ||
if (server) { | ||
this.server = server; | ||
this.server.on('request', this._requestHandler); | ||
} | ||
else { | ||
throw new Error("You must specify a WebSocketServer instance to attach to."); | ||
} | ||
}; | ||
var pathString; | ||
if (typeof(path) === 'string') { | ||
if (path === '*') { | ||
path = /^.*$/; | ||
pathString = '*' | ||
} | ||
else { | ||
path = new RegExp('^' + path + '$'); | ||
pathString = path.toString(); | ||
} | ||
} | ||
if (!(path instanceof RegExp)) { | ||
throw new Error("Path must be specified as either a string or a RegExp."); | ||
} | ||
// normalize protocol to lower-case | ||
protocol = protocol.toLocaleLowerCase(); | ||
WebSocketRouter.prototype.detachServer = function() { | ||
if (this.server) { | ||
this.server.removeListener('request', this._requestHandler); | ||
this.server = null; | ||
} | ||
else { | ||
throw new Error("Cannot detach from server: not attached.") | ||
} | ||
}; | ||
if (this.findHandlerIndex(pathString, protocol) !== -1) { | ||
throw new Error("You may only mount one handler per path/protocol combination."); | ||
} | ||
WebSocketRouter.prototype.mount = function(path, protocol, callback) { | ||
if (!path) { | ||
throw new Error("You must specify a path for this handler."); | ||
} | ||
if (!protocol) { | ||
protocol = "____no_protocol____"; | ||
} | ||
if (!callback) { | ||
throw new Error("You must specify a callback for this handler."); | ||
} | ||
this.handlers.push({ | ||
'path': path, | ||
'pathString': pathString, | ||
'protocol': protocol, | ||
'callback': callback | ||
}); | ||
}, | ||
unmount: function(path, protocol) { | ||
var index = this.findHandlerIndex(path.toString(), protocol); | ||
if (index !== -1) { | ||
this.handlers.splice(index, 1); | ||
var pathString; | ||
if (typeof(path) === 'string') { | ||
if (path === '*') { | ||
path = /^.*$/; | ||
pathString = '*' | ||
} | ||
else { | ||
throw new Error("Unable to find a route matching the specified path and protocol."); | ||
path = new RegExp('^' + path + '$'); | ||
pathString = path.toString(); | ||
} | ||
}, | ||
} | ||
if (!(path instanceof RegExp)) { | ||
throw new Error("Path must be specified as either a string or a RegExp."); | ||
} | ||
findHandlerIndex: function(pathString, protocol) { | ||
protocol = protocol.toLocaleLowerCase(); | ||
for (var i=0, len=this.handlers.length; i < len; i++) { | ||
var handler = this.handlers[i]; | ||
if (handler.pathString === pathString && handler.protocol === protocol) { | ||
return i; | ||
} | ||
// normalize protocol to lower-case | ||
protocol = protocol.toLocaleLowerCase(); | ||
if (this.findHandlerIndex(pathString, protocol) !== -1) { | ||
throw new Error("You may only mount one handler per path/protocol combination."); | ||
} | ||
this.handlers.push({ | ||
'path': path, | ||
'pathString': pathString, | ||
'protocol': protocol, | ||
'callback': callback | ||
}); | ||
}; | ||
WebSocketRouter.prototype.unmount = function(path, protocol) { | ||
var index = this.findHandlerIndex(path.toString(), protocol); | ||
if (index !== -1) { | ||
this.handlers.splice(index, 1); | ||
} | ||
else { | ||
throw new Error("Unable to find a route matching the specified path and protocol."); | ||
} | ||
}; | ||
WebSocketRouter.prototype.findHandlerIndex = function(pathString, protocol) { | ||
protocol = protocol.toLocaleLowerCase(); | ||
for (var i=0, len=this.handlers.length; i < len; i++) { | ||
var handler = this.handlers[i]; | ||
if (handler.pathString === pathString && handler.protocol === protocol) { | ||
return i; | ||
} | ||
return -1; | ||
}, | ||
} | ||
return -1; | ||
}; | ||
WebSocketRouter.prototype.handleRequest = function(request) { | ||
var requestedProtocols = request.requestedProtocols; | ||
if (requestedProtocols.length === 0) { | ||
requestedProtocols = ['____no_protocol____']; | ||
} | ||
handleRequest: function(request) { | ||
var requestedProtocols = request.requestedProtocols; | ||
if (requestedProtocols.length === 0) { | ||
requestedProtocols = ['____no_protocol____']; | ||
} | ||
// Find a handler with the first requested protocol first | ||
for (var i=0; i < requestedProtocols.length; i++) { | ||
var requestedProtocol = requestedProtocols[i]; | ||
// Find a handler with the first requested protocol first | ||
for (var i=0; i < requestedProtocols.length; i++) { | ||
var requestedProtocol = requestedProtocols[i]; | ||
// find the first handler that can process this request | ||
for (var j=0, len=this.handlers.length; j < len; j++) { | ||
var handler = this.handlers[j]; | ||
if (handler.path.test(request.resource)) { | ||
if (requestedProtocol === handler.protocol || | ||
handler.protocol === '*') | ||
{ | ||
var routerRequest = new WebSocketRouterRequest(request, requestedProtocol); | ||
handler.callback(routerRequest); | ||
return; | ||
} | ||
// find the first handler that can process this request | ||
for (var j=0, len=this.handlers.length; j < len; j++) { | ||
var handler = this.handlers[j]; | ||
if (handler.path.test(request.resource)) { | ||
if (requestedProtocol === handler.protocol || | ||
handler.protocol === '*') | ||
{ | ||
var routerRequest = new WebSocketRouterRequest(request, requestedProtocol); | ||
handler.callback(routerRequest); | ||
return; | ||
} | ||
} | ||
} | ||
// If we get here we were unable to find a suitable handler. | ||
request.reject(404, "No handler is available for the given request."); | ||
} | ||
}); | ||
// If we get here we were unable to find a suitable handler. | ||
request.reject(404, "No handler is available for the given request."); | ||
}; | ||
module.exports = WebSocketRouter; |
@@ -1,3 +0,1 @@ | ||
var extend = require('./utils').extend; | ||
function WebSocketRouterRequest(webSocketRequest, resolvedProtocol) { | ||
@@ -16,11 +14,10 @@ this.webSocketRequest = webSocketRequest; | ||
extend(WebSocketRouterRequest.prototype, { | ||
accept: function(origin) { | ||
return this.webSocketRequest.accept(this.protocol, origin); | ||
}, | ||
reject: function(status, reason) { | ||
return this.webSocketRequest.reject(status, reason); | ||
} | ||
}); | ||
WebSocketRouterRequest.prototype.accept = function(origin) { | ||
return this.webSocketRequest.accept(this.protocol, origin); | ||
}; | ||
WebSocketRouterRequest.prototype.reject = function(status, reason) { | ||
return this.webSocketRequest.reject(status, reason); | ||
}; | ||
module.exports = WebSocketRouterRequest; |
@@ -20,122 +20,119 @@ var extend = require('./utils').extend; | ||
extend(WebSocketServer.prototype, { | ||
mount: function(config) { | ||
this.config = { | ||
// The http server instance to attach to. Required. | ||
httpServer: null, | ||
// 64KiB max frame size. | ||
maxReceivedFrameSize: 0x10000, | ||
// 1MiB max message size, only applicable if | ||
// assembleFragments is true | ||
maxReceivedMessageSize: 0x100000, | ||
// Outgoing messages larger than fragmentationThreshold will be | ||
// split into multiple fragments. | ||
fragmentOutgoingMessages: true, | ||
// Outgoing frames are fragmented if they exceed this threshold. | ||
// Default is 16KiB | ||
fragmentationThreshold: 0x4000, | ||
// If true, the server will automatically send a ping to all | ||
// clients every 'keepaliveInterval' milliseconds. | ||
keepalive: true, | ||
// The interval to send keepalive pings to connected clients. | ||
keepaliveInterval: 20000, | ||
// If true, fragmented messages will be automatically assembled | ||
// and the full message will be emitted via a 'message' event. | ||
// If false, each frame will be emitted via a 'frame' event and | ||
// the application will be responsible for aggregating multiple | ||
// fragmented frames. Single-frame messages will emit a 'message' | ||
// event in addition to the 'frame' event. | ||
// Most users will want to leave this set to 'true' | ||
assembleFragments: true, | ||
// If this is true, websocket connections will be accepted | ||
// regardless of the path and protocol specified by the client. | ||
// The protocol accepted will be the first that was requested | ||
// by the client. Clients from any origin will be accepted. | ||
// This should only be used in the simplest of cases. You should | ||
// probably leave this set to 'false' and inspect the request | ||
// object to make sure it's acceptable before accepting it. | ||
autoAcceptConnections: false, | ||
// The number of milliseconds to wait after sending a close frame | ||
// for an acknowledgement to come back before giving up and just | ||
// closing the socket. | ||
closeTimeout: 5000 | ||
}; | ||
extend(this.config, config); | ||
WebSocketServer.prototype.mount = function(config) { | ||
this.config = { | ||
// The http server instance to attach to. Required. | ||
httpServer: null, | ||
// this.httpServer = httpServer; | ||
// if (typeof(pathRegExp) === 'string') { | ||
// pathRegExp = new RegExp('^' + pathRegExp + '$'); | ||
// } | ||
// this.pathRegExp = pathRegExp; | ||
// this.protocol = protocol; | ||
if (this.config.httpServer) { | ||
this.config.httpServer.on('upgrade', this._handlers.upgrade); | ||
} | ||
else { | ||
throw new Error("You must specify an httpServer on which to mount the WebSocket server.") | ||
} | ||
}, | ||
// 64KiB max frame size. | ||
maxReceivedFrameSize: 0x10000, | ||
unmount: function() { | ||
this.config.httpServer.removeListener('upgrade', this._handlers.upgrade); | ||
}, | ||
closeAllConnections: function() { | ||
this.connections.forEach(function(connection) { | ||
connection.close(); | ||
}); | ||
}, | ||
shutDown: function() { | ||
this.unmount(); | ||
this.closeAllConnections(); | ||
}, | ||
handleUpgrade: function(request, socket, head) { | ||
var wsRequest = new WebSocketRequest(socket, request, this.config); | ||
wsRequest.once('requestAccepted', this._handlers.requestAccepted); | ||
// 1MiB max message size, only applicable if | ||
// assembleFragments is true | ||
maxReceivedMessageSize: 0x100000, | ||
try { | ||
wsRequest.readHandshake(request); | ||
} | ||
catch(e) { | ||
console.error((new Date()) + " WebSocket: Invalid handshake: " + e.toString()); | ||
return; | ||
} | ||
// Outgoing messages larger than fragmentationThreshold will be | ||
// split into multiple fragments. | ||
fragmentOutgoingMessages: true, | ||
if (!this.config.autoAcceptConnections && this.listeners('request').length > 0) { | ||
this.emit('request', wsRequest); | ||
} | ||
else if (this.config.autoAcceptConnections) { | ||
wsRequest.accept(wsRequest.requestedProtocols[0], wsRequest.origin); | ||
} | ||
else { | ||
wsRequest.reject(404, "No handler is configured to accept the connection."); | ||
} | ||
}, | ||
// Outgoing frames are fragmented if they exceed this threshold. | ||
// Default is 16KiB | ||
fragmentationThreshold: 0x4000, | ||
// If true, the server will automatically send a ping to all | ||
// clients every 'keepaliveInterval' milliseconds. | ||
keepalive: true, | ||
// The interval to send keepalive pings to connected clients. | ||
keepaliveInterval: 20000, | ||
// If true, fragmented messages will be automatically assembled | ||
// and the full message will be emitted via a 'message' event. | ||
// If false, each frame will be emitted via a 'frame' event and | ||
// the application will be responsible for aggregating multiple | ||
// fragmented frames. Single-frame messages will emit a 'message' | ||
// event in addition to the 'frame' event. | ||
// Most users will want to leave this set to 'true' | ||
assembleFragments: true, | ||
// If this is true, websocket connections will be accepted | ||
// regardless of the path and protocol specified by the client. | ||
// The protocol accepted will be the first that was requested | ||
// by the client. Clients from any origin will be accepted. | ||
// This should only be used in the simplest of cases. You should | ||
// probably leave this set to 'false' and inspect the request | ||
// object to make sure it's acceptable before accepting it. | ||
autoAcceptConnections: false, | ||
// The number of milliseconds to wait after sending a close frame | ||
// for an acknowledgement to come back before giving up and just | ||
// closing the socket. | ||
closeTimeout: 5000 | ||
}; | ||
extend(this.config, config); | ||
handleRequestAccepted: function(connection) { | ||
connection.once('close', this._handlers.connectionClose); | ||
this.connections.push(connection); | ||
this.emit('connect', connection); | ||
}, | ||
// this.httpServer = httpServer; | ||
// if (typeof(pathRegExp) === 'string') { | ||
// pathRegExp = new RegExp('^' + pathRegExp + '$'); | ||
// } | ||
// this.pathRegExp = pathRegExp; | ||
// this.protocol = protocol; | ||
if (this.config.httpServer) { | ||
this.config.httpServer.on('upgrade', this._handlers.upgrade); | ||
} | ||
else { | ||
throw new Error("You must specify an httpServer on which to mount the WebSocket server.") | ||
} | ||
}; | ||
WebSocketServer.prototype.unmount = function() { | ||
this.config.httpServer.removeListener('upgrade', this._handlers.upgrade); | ||
}; | ||
WebSocketServer.prototype.closeAllConnections = function() { | ||
this.connections.forEach(function(connection) { | ||
connection.close(); | ||
}); | ||
}; | ||
WebSocketServer.prototype.shutDown = function() { | ||
this.unmount(); | ||
this.closeAllConnections(); | ||
}; | ||
WebSocketServer.prototype.handleUpgrade = function(request, socket, head) { | ||
var wsRequest = new WebSocketRequest(socket, request, this.config); | ||
wsRequest.once('requestAccepted', this._handlers.requestAccepted); | ||
handleConnectionClose: function(connection) { | ||
var index = this.connections.indexOf(connection); | ||
if (index !== -1) { | ||
this.connections.splice(index, 1); | ||
} | ||
this.emit('close', connection); | ||
try { | ||
wsRequest.readHandshake(request); | ||
} | ||
}); | ||
catch(e) { | ||
console.error((new Date()) + " WebSocket: Invalid handshake: " + e.toString()); | ||
return; | ||
} | ||
if (!this.config.autoAcceptConnections && this.listeners('request').length > 0) { | ||
this.emit('request', wsRequest); | ||
} | ||
else if (this.config.autoAcceptConnections) { | ||
wsRequest.accept(wsRequest.requestedProtocols[0], wsRequest.origin); | ||
} | ||
else { | ||
wsRequest.reject(404, "No handler is configured to accept the connection."); | ||
} | ||
}; | ||
WebSocketServer.prototype.handleRequestAccepted = function(connection) { | ||
connection.once('close', this._handlers.connectionClose); | ||
this.connections.push(connection); | ||
this.emit('connect', connection); | ||
}; | ||
WebSocketServer.prototype.handleConnectionClose = function(connection) { | ||
var index = this.connections.indexOf(connection); | ||
if (index !== -1) { | ||
this.connections.splice(index, 1); | ||
} | ||
this.emit('close', connection); | ||
}; | ||
module.exports = WebSocketServer; |
@@ -6,3 +6,3 @@ { | ||
"author": "Brian McKelvey <brian@worlize.com>", | ||
"version": "0.0.4", | ||
"version": "0.0.5", | ||
"repository": { | ||
@@ -9,0 +9,0 @@ "type": "git", |
@@ -52,5 +52,9 @@ #!/usr/bin/env node | ||
var messageSize = 0; | ||
var startTime; | ||
var byteCounter; | ||
client.on('connect', function(connection) { | ||
console.log("Connected"); | ||
startTime = new Date(); | ||
byteCounter = 0; | ||
@@ -68,2 +72,3 @@ connection.on('error', function(error) { | ||
console.log("Received utf-8 message of " + message.utf8Data.length + " characters."); | ||
logThroughput(message.utf8Data.length); | ||
requestData(); | ||
@@ -73,2 +78,3 @@ } | ||
console.log("Received binary message of " + message.binaryData.length + " bytes."); | ||
logThroughput(message.binaryData.length); | ||
requestData(); | ||
@@ -83,2 +89,3 @@ } | ||
console.log("Total message size: " + messageSize + " bytes."); | ||
logThroughput(messageSize); | ||
messageSize = 0; | ||
@@ -89,2 +96,13 @@ requestData(); | ||
function logThroughput(numBytes) { | ||
byteCounter += numBytes; | ||
var duration = (new Date()).valueOf() - startTime.valueOf(); | ||
if (duration > 1000) { | ||
var kiloBytesPerSecond = Math.round((byteCounter / 1024) / (duration/1000)); | ||
console.log(" Throughput: " + kiloBytesPerSecond + " KBps"); | ||
startTime = new Date(); | ||
byteCounter = 0; | ||
} | ||
}; | ||
function requestData() { | ||
@@ -91,0 +109,0 @@ if (args.binary) { |
@@ -135,3 +135,3 @@ #!/usr/bin/env node | ||
console.log("Point your draft-08 compliant browser at http://localhost:" + args.port + "/"); | ||
console.log("Point your draft-09 compliant browser at http://localhost:" + args.port + "/"); | ||
if (args['no-fragmentation']) { | ||
@@ -138,0 +138,0 @@ console.log("Fragmentation disabled."); |
6665
252300