Compare commits

..

50 Commits

Author SHA1 Message Date
James Coglan 1c33c0ce56 Bump version to 0.3.6. 2014-10-04 08:30:43 +01:00
James Coglan f693b02743 Merge pull request #8 from meteor/close-before-start
driver.close() before driver.start() should close
2014-09-07 21:29:25 +01:00
James Coglan 91f19cb69b Use the SVG build status icon from Travis. 2014-09-07 21:06:45 +01:00
David Glasser dcd7c278b6 driver.close() before driver.start() should close 2014-08-15 10:48:30 -07:00
James Coglan dbedb370b7 Bump version to 0.3.5. 2014-07-06 10:18:19 +01:00
James Coglan 28858d9fb3 Delegate the protocol and version methods correctly in the TCP driver. 2014-07-04 17:49:11 +01:00
James Coglan ae52cf995e Stop the Hybi parser hanging onto data it doesn't need after emitFrame(). 2014-06-16 20:30:44 +01:00
James Coglan dfe7b2b000 Trailing blank lines are bad. 2014-05-17 00:35:31 +01:00
James Coglan d40de878b2 Bump version to 0.3.4. 2014-05-08 02:19:58 +01:00
James Coglan ed3907d5fd Clean up the state management code in StreamReader. 2014-05-07 20:49:49 +01:00
James Coglan 84b6f50f1a Merge pull request #6 from meteor/forget-finished-buffers
StreamReader: Don't hang on to fully read buffers
2014-05-07 20:41:15 +01:00
David Glasser 8d19ee823e StreamReader: Don't hang on to fully read buffers 2014-05-06 11:52:59 -07:00
James Coglan 3bee0366c5 Bump version to 0.3.3. 2014-04-24 23:31:43 +01:00
James Coglan 9f2782da14 Correct the draft-76 status line reason phrase. 2014-04-17 02:23:58 +01:00
James Coglan c752c76712 Bump version to 0.3.2. 2013-12-29 12:21:49 +00:00
James Coglan ad8f08f19e Make Node 0.6 work on Travis. 2013-12-28 17:40:18 +00:00
James Coglan d343c7d21b Document the new lower frame size limit. 2013-12-28 16:56:15 +00:00
James Coglan 0b089ad921 Merge pull request #5 from Zarel/patch-1
Lower default max buffer length
2013-12-27 10:17:09 -08:00
James Coglan e65e837968 Stop parsing Hybi when we go into a failure state. 2013-12-27 18:16:41 +00:00
Guangcong Luo cda22e3bef Lower default max buffer length
Real-world testing shows that the previous max buffer length
of ~1GB was too high, and messages of the size of roughly 300MB
could still cause the process to crash from running out of
memory. 64MB seems like a reasonably conservative maximum.
2013-12-24 19:26:38 -06:00
James Coglan 1ae37d6efe Remove an unused instance variable _lengthBuffer. 2013-12-20 10:20:41 +00:00
James Coglan 82c42a6ce5 Extend max-length checking to draft-{75,76}. 2013-12-20 10:09:31 +00:00
James Coglan aad1519f3f Extend max-length checking to short (<= 125 bytes) frames and to sequences of continuation frames. 2013-12-20 09:50:18 +00:00
James Coglan 11a9b75185 Bump version to 0.3.1. 2013-12-03 00:46:42 +00:00
James Coglan d93c853414 Don't pre-allocate a huge Buffer before we know there's enough chunks in the read queue to complete a message. 2013-12-02 21:07:33 +00:00
James Coglan 2dff35d3e4 Map Node v0.11's magic numbers for HTTP methods. 2013-12-02 13:02:38 +00:00
James Coglan 6588928445 Make HttpParser work on v0.11.6+. 2013-11-29 00:42:49 +00:00
James Coglan 151fddd206 Make the maximum frame length into an option that the user can set. 2013-11-28 23:38:42 +00:00
James Coglan 0b1f16a7ee Merge pull request #2 from meteor/limit-frame-to-max-buffer
Reduce frame max length to max Node Buffer length.
2013-11-28 15:25:05 -08:00
David Glasser f15b331a34 Reduce frame max length to max Node Buffer length. 2013-10-11 19:21:14 -07:00
James Coglan 95261a1779 Bump version to 0.3.0. 2013-09-09 20:15:10 +01:00
James Coglan 12f9f4d444 Add support for Basic Auth URLs to the client driver. 2013-08-13 17:16:49 +01:00
James Coglan d3e81b478e Bump version to 0.2.2. 2013-07-05 15:16:37 +01:00
James Coglan c5b3df986b Migrate to jstest. 2013-07-01 02:10:29 +01:00
James Coglan 286dea4337 Write the reflexive pipe examples as one-liners. [http://nodejsreactions.tumblr.com/post/51566814133/src-pipe-dst-pipe-src] 2013-05-29 02:24:27 +01:00
James Coglan 2378f4c484 An example in the README was missing a closing paren. 2013-05-17 12:46:35 +01:00
James Coglan 5a1dcd5773 Bump version to 0.2.1. 2013-05-17 11:51:34 +01:00
James Coglan 17e2bd4084 Messages should be queued when the driver is in the 'pre-connecting' state, as the client is initially. 2013-05-17 11:48:36 +01:00
James Coglan 3dfaaf497f Expose the isSecureRequest() method. 2013-05-17 11:47:56 +01:00
James Coglan 0e4f13d0e4 Bump version to 0.2.0. 2013-05-12 15:09:18 +01:00
James Coglan 4f8e10ead6 Add a changelog. 2013-05-12 15:08:17 +01:00
James Coglan 97451f81ab Set status and headers as soon as we have them so they're available if there is an error. 2013-05-12 14:44:17 +01:00
James Coglan 390ebee4d8 Parameterize the port we run the example server on. 2013-05-12 13:19:13 +01:00
James Coglan de001a0f54 Remove a blank line. 2013-05-12 01:45:56 +01:00
James Coglan 26c4873f3e Apply offset to the consumed offset from HTTP parsing before we check whether the whole buffer has been used. 2013-05-12 01:45:04 +01:00
James Coglan b2472e89a6 Document the Server driver. 2013-05-12 01:31:14 +01:00
James Coglan 1124df2bf5 Add a Server driver for running WebSockets through a bare TCP server. 2013-05-12 01:13:50 +01:00
James Coglan ea4e289669 Add a setHeader() function to allow peers to add headers to their handshake responses. 2013-05-11 22:26:20 +01:00
James Coglan d7c65eedd9 Expose the status and headers from the response in the client driver. 2013-05-11 21:58:37 +01:00
James Coglan d594f7d708 Include more possible headers as indicators of a secure connection. 2013-05-11 21:55:52 +01:00
22 changed files with 662 additions and 232 deletions
+2
View File
@@ -6,3 +6,5 @@ node_js:
- "0.10"
- "0.11"
before_install:
- '[ "${TRAVIS_NODE_VERSION}" = "0.6" ] && npm conf set strict-ssl false || true'
+50
View File
@@ -0,0 +1,50 @@
### 0.3.6 / 2014-10-04
* It is now possible to call `close()` before `start()` and close the driver
### 0.3.5 / 2014-07-06
* Don't hold references to frame buffers after a message has been emitted
* Make sure that `protocol` and `version` are exposed properly by the TCP driver
### 0.3.4 / 2014-05-08
* Don't hold memory-leaking references to I/O buffers after they have been parsed
### 0.3.3 / 2014-04-24
* Correct the draft-76 status line reason phrase
### 0.3.2 / 2013-12-29
* Expand `maxLength` to cover sequences of continuation frames and `draft-{75,76}`
* Decrease default maximum frame buffer size to 64MB
* Stop parsing when the protocol enters a failure mode, to save CPU cycles
### 0.3.1 / 2013-12-03
* Add a `maxLength` option to limit allowed frame size
* Don't pre-allocate a message buffer until the whole frame has arrived
* Fix compatibility with Node v0.11 `HTTPParser`
### 0.3.0 / 2013-09-09
* Support client URLs with Basic Auth credentials
### 0.2.2 / 2013-07-05
* No functional changes, just updates to package.json
### 0.2.1 / 2013-05-17
* Export the isSecureRequest() method since faye-websocket relies on it
* Queue sent messages in the client's initial state
### 0.2.0 / 2013-05-12
* Add API for setting and reading headers
* Add Driver.server() method for getting a driver for TCP servers
### 0.1.0 / 2013-05-04
* First stable release
+71 -11
View File
@@ -1,4 +1,4 @@
# websocket-driver [![Build Status](https://travis-ci.org/faye/websocket-driver-node.png)](https://travis-ci.org/faye/websocket-driver-node)
# websocket-driver [![Build Status](https://travis-ci.org/faye/websocket-driver-node.svg)](https://travis-ci.org/faye/websocket-driver-node)
This module provides a complete implementation of the WebSocket protocols that
can be hooked up to any I/O stream. It aims to simplify things by decoupling
@@ -43,7 +43,7 @@ streams attached; one for incoming/outgoing messages and one for managing the
wire protocol over an I/O stream. The full API is described below.
### Server-side
### Server-side with HTTP
A Node webserver emits a special event for 'upgrade' requests, and this is
where you should handle WebSockets. You first check whether the request is a
@@ -62,21 +62,66 @@ server.on('upgrade', function(request, socket, body) {
var driver = websocket.http(request);
driver.io.write(body);
socket.pipe(driver.io);
driver.io.pipe(socket);
socket.pipe(driver.io).pipe(socket);
driver.messages.on('data', function(message) {
console.log('Got a message', message);
});
driver.start();
};
});
```
Note the line `driver.io.write(body)` - you must pass the `body` buffer to the
socket driver in order to make certain versions of the protocol work.
### Server-side with TCP
You can also handle WebSocket connections in a bare TCP server, if you're not
using an HTTP server and don't want to implement HTTP parsing yourself.
The driver will emit a `connect` event when a request is received, and at this
point you can detect whether it's a WebSocket and handle it as such. Here's an
example using the Node `net` module:
```js
var net = require('net'),
websocket = require('websocket-driver');
var server = net.createServer(function(connection) {
var driver = websocket.server();
driver.on('connect', function() {
if (websocket.isWebSocket(driver)) {
driver.start();
} else {
// handle other HTTP requests
}
});
driver.on('close', function() { connection.end() });
connection.on('error', function() {});
connection.pipe(driver.io).pipe(connection);
driver.messages.pipe(driver.messages);
});
server.listen(4180);
```
In the `connect` event, the driver gains several properties to describe the
request, similar to a Node request object, such as `method`, `url` and
`headers`. However you should remember it's not a real request object; you
cannot write data to it, it only tells you what request data we parsed from the
input.
If the request has a body, it will be in the `driver.body` buffer, but only as
much of the body as has been piped into the driver when the `connect` event
fires.
### Client-side
Similarly, to implement a WebSocket client you just need to make a driver by
@@ -91,8 +136,7 @@ var net = require('net'),
var driver = websocket.client('ws://www.example.com/socket'),
tcp = net.createConnection(80, 'www.example.com');
tcp.pipe(driver.io);
driver.io.pipe(tcp);
tcp.pipe(driver.io).pipe(tcp);
driver.messages.on('data', function(message) {
console.log('Got a message', message);
@@ -103,6 +147,12 @@ tcp.on('connect', function() {
});
```
Client drivers have two additional properties for reading the HTTP data that
was sent back by the server:
* `driver.statusCode` - the integer value of the HTTP status code
* `driver.headers` - an object containing the response headers
### Driver API
@@ -110,16 +160,21 @@ Drivers are created using one of the following methods:
```js
driver = websocket.http(request, options)
driver = websocket.server(options)
driver = websocket.client(url, options)
```
The `http` method returns a driver chosen using the headers from a Node HTTP
request object. The `client` method always returns a driver for the RFC version
of the protocol with masking enabled on outgoing frames.
request object. The `server` method returns a driver that will parse an HTTP
request and then decide which driver to use for it using the `http` method. The
`client` method always returns a driver for the RFC version of the protocol
with masking enabled on outgoing frames.
The `options` argument is optional, and is an object. It may contain the
following fields:
* `maxLength` - the maximum allowed size of incoming message frames, in bytes.
The default value is `2^26 - 1`, or 1 byte short of 64 MiB.
* `protocols` - an array of strings representing acceptable subprotocols for
use over the socket. The driver will negotiate one of these to use via the
`Sec-WebSocket-Protocol` header if supported by the other peer.
@@ -164,6 +219,12 @@ describing the error.
Sets the callback to execute when the socket becomes closed. The `event` object
has `code` and `reason` attributes.
#### `driver.setHeader(name, value)`
Sets a custom header to be sent as part of the handshake response, either from
the server or from the client. Must be called before `start()`, since this is
when the headers are serialized and sent.
#### `driver.start()`
Initiates the protocol by sending the handshake - either the response for a
@@ -224,7 +285,7 @@ after `emit('open')` has fired.
(The MIT License)
Copyright (c) 2010-2013 James Coglan
Copyright (c) 2010-2014 James Coglan
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the 'Software'), to deal in
@@ -243,4 +304,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+20
View File
@@ -0,0 +1,20 @@
var net = require('net'),
websocket = require('../lib/websocket/driver');
var server = net.createServer(function(connection) {
var driver = websocket.server();
driver.on('connect', function() {
if (websocket.isWebSocket(driver)) driver.start();
});
driver.on('close', function() { connection.end() });
connection.on('error', function() {});
connection.pipe(driver.io);
driver.io.pipe(connection);
driver.messages.pipe(driver.messages);
});
server.listen(process.argv[2]);
+10 -28
View File
@@ -4,45 +4,28 @@
// * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
// * http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17
var Draft75 = require('./driver/draft75'),
Draft76 = require('./driver/draft76'),
Hybi = require('./driver/hybi'),
Client = require('./driver/client');
var Client = require('./driver/client'),
Server = require('./driver/server');
var Driver = {
isSecureRequest: function(request) {
if (request.headers['x-forwarded-proto']) {
return request.headers['x-forwarded-proto'] === 'https';
} else {
return (request.connection && request.connection.authorized !== undefined) ||
(request.socket && request.socket.secure);
}
},
determineUrl: function(request) {
var scheme = this.isSecureRequest(request) ? 'wss:' : 'ws:';
return scheme + '//' + request.headers.host + request.url;
},
client: function(url, options) {
options = options || {};
if (options.masking === undefined) options.masking = true;
return new Client(url, options);
},
http: function(request, options) {
server: function(options) {
options = options || {};
if (options.requireMasking === undefined) options.requireMasking = true;
return new Server(options);
},
var headers = request.headers,
url = this.determineUrl(request);
http: function() {
return Server.http.apply(Server, arguments);
},
if (headers['sec-websocket-version'])
return new Hybi(request, url, options);
else if (headers['sec-websocket-key1'])
return new Draft76(request, url, options);
else
return new Draft75(request, url, options);
isSecureRequest: function(request) {
return Server.isSecureRequest(request);
},
isWebSocket: function(request) {
@@ -58,4 +41,3 @@ var Driver = {
};
module.exports = Driver;
+41 -24
View File
@@ -1,50 +1,66 @@
var Emitter = require('events').EventEmitter,
util = require('util'),
streams = require('../streams');
streams = require('../streams'),
Headers = require('./headers');
var Base = function(request, url, options) {
Emitter.call(this);
this._request = request;
this._options = options || {};
this._maxLength = this._options.maxLength || this.MAX_LENGTH;
this.__headers = new Headers();
this.__queue = [];
this.readyState = 0;
this.url = url;
var self = this;
this.io = new streams.IO(this);
this.messages = new streams.Messages(this);
// Protocol errors are informational and do not have to be handled
this.messages.on('error', function() {});
this.on('message', function(event) {
var messages = self.messages;
if (messages.readable) messages.emit('data', event.data);
});
this.on('error', function(error) {
var messages = self.messages;
if (messages.readable) messages.emit('error', error);
});
this.on('close', function() {
var messages = self.messages;
if (!messages.readable) return;
messages.readable = messages.writable = false;
messages.emit('end');
});
this._bindEventListeners();
};
util.inherits(Base, Emitter);
var instance = {
// This is 64MB, small enough for an average VPS to handle without
// crashing from process out of memory
MAX_LENGTH: 0x3ffffff,
STATES: ['connecting', 'open', 'closing', 'closed'],
_bindEventListeners: function() {
var self = this;
// Protocol errors are informational and do not have to be handled
this.messages.on('error', function() {});
this.on('message', function(event) {
var messages = self.messages;
if (messages.readable) messages.emit('data', event.data);
});
this.on('error', function(error) {
var messages = self.messages;
if (messages.readable) messages.emit('error', error);
});
this.on('close', function() {
var messages = self.messages;
if (!messages.readable) return;
messages.readable = messages.writable = false;
messages.emit('end');
});
},
getState: function() {
return this.STATES[this.readyState] || null;
},
setHeader: function(name, value) {
if (this.readyState > 0) return false;
this.__headers.set(name, value);
return true;
},
start: function() {
if (this.readyState !== 0) return false;
this._write(this._handshakeResponse());
@@ -93,6 +109,8 @@ for (var key in instance)
Base.prototype[key] = instance[key];
Base.ConnectEvent = function() {};
Base.OpenEvent = function() {};
Base.CloseEvent = function(code, reason) {
@@ -105,4 +123,3 @@ Base.MessageEvent = function(data) {
};
module.exports = Base;
+22 -40
View File
@@ -1,6 +1,6 @@
var HTTPParser = process.binding('http_parser').HTTPParser,
url = require('url'),
var url = require('url'),
util = require('util'),
HttpParser = require('./http_parser'),
Base = require('./base'),
Hybi = require('./hybi');
@@ -11,31 +11,7 @@ var Client = function(url, options) {
this.readyState = -1;
this._key = Client.generateKey();
this._accept = Hybi.generateAccept(this._key);
this._http = new HTTPParser(HTTPParser.RESPONSE || 'response');
this._node = HTTPParser.RESPONSE ? 6 : 4;
this._complete = false;
this._headers = {};
var currentHeader = null,
self = this;
this._http.onHeaderField = function(b, start, length) {
currentHeader = b.toString('utf8', start, start + length);
};
this._http.onHeaderValue = function(b, start, length) {
self._headers[currentHeader] = b.toString('utf8', start, start + length);
};
this._http.onHeadersComplete = function(info) {
self._status = info.statusCode;
var headers = info.headers;
if (!headers) return;
for (var i = 0, n = headers.length; i < n; i += 2)
self._headers[headers[i]] = headers[i+1];
};
this._http.onMessageComplete = function() {
self._complete = true;
};
this._http = new HttpParser('response');
};
util.inherits(Client, Hybi);
@@ -56,11 +32,11 @@ var instance = {
parse: function(data) {
if (this.readyState > 0) return Hybi.prototype.parse.call(this, data);
var consumed = this._http.execute(data, 0, data.length),
offset = (this._node < 6) ? 1 : 0;
if (consumed <= data.length) this._validateHandshake();
if (consumed < data.length) this.parse(data.slice(consumed + offset));
this._http.parse(data);
if (!this._http.isComplete()) return;
this._validateHandshake();
this.parse(this._http.body);
},
_handshakeRequest: function() {
@@ -77,7 +53,10 @@ var instance = {
if (this._protocols.length > 0)
headers.push('Sec-WebSocket-Protocol: ' + this._protocols.join(', '));
return new Buffer(headers.concat('','').join('\r\n'), 'utf8');
if (uri.auth)
headers.push('Authorization: Basic ' + new Buffer(uri.auth, 'utf8').toString('base64'));
return new Buffer(headers.concat(this.__headers.toString(), '').join('\r\n'), 'utf8');
},
_failHandshake: function(message) {
@@ -88,13 +67,17 @@ var instance = {
},
_validateHandshake: function() {
if (this._status !== 101)
return this._failHandshake('Unexpected response code: ' + this._status);
this.statusCode = this._http.statusCode;
this.headers = this._http.headers;
var upgrade = this._headers.Upgrade || '',
connection = this._headers.Connection || '',
accept = this._headers['Sec-WebSocket-Accept'] || '',
protocol = this._headers['Sec-WebSocket-Protocol'] || '';
if (this._http.statusCode !== 101)
return this._failHandshake('Unexpected response code: ' + this._http.statusCode);
var headers = this._http.headers,
upgrade = headers['upgrade'] || '',
connection = headers['connection'] || '',
accept = headers['sec-websocket-accept'] || '',
protocol = headers['sec-websocket-protocol'] || '';
if (upgrade === '')
return this._failHandshake("'Upgrade' header is missing");
@@ -126,4 +109,3 @@ for (var key in instance)
Client.prototype[key] = instance[key];
module.exports = Client;
+12 -3
View File
@@ -9,7 +9,16 @@ var Draft75 = function(request, url, options) {
util.inherits(Draft75, Base);
var instance = {
close: function() {
if (this.readyState === 3) return false;
this.readyState = 3;
this.emit('close', new Base.CloseEvent(null, null));
return true;
},
parse: function(buffer) {
if (this.readyState > 1) return;
var data, message, value;
for (var i = 0, n = buffer.length; i < n; i++) {
data = buffer[i];
@@ -29,8 +38,7 @@ var instance = {
this._length = value + 128 * this._length;
if (this._closing && this._length === 0) {
this.readyState = 3;
this.emit('close', new Base.CloseEvent(null, null));
return this.close();
}
else if ((0x80 & data) !== 0x80) {
if (this._length === 0) {
@@ -56,6 +64,7 @@ var instance = {
this._stage = 0;
} else {
this._buffer.push(data);
if (this._buffer.length > this._maxLength) return this.close();
}
}
break;
@@ -84,6 +93,7 @@ var instance = {
'Connection: Upgrade\r\n' +
'WebSocket-Origin: ' + this._request.headers.origin + '\r\n' +
'WebSocket-Location: ' + this.url + '\r\n' +
this.__headers.toString() +
'\r\n',
'utf8');
},
@@ -105,4 +115,3 @@ for (var key in instance)
Draft75.prototype[key] = instance[key];
module.exports = Draft75;
+2 -2
View File
@@ -48,11 +48,12 @@ var instance = {
},
_handshakeResponse: function() {
return new Buffer('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
return new Buffer('HTTP/1.1 101 WebSocket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' +
'Sec-WebSocket-Origin: ' + this._request.headers.origin + '\r\n' +
'Sec-WebSocket-Location: ' + this.url + '\r\n' +
this.__headers.toString() +
'\r\n',
'binary');
},
@@ -105,4 +106,3 @@ for (var key in instance)
Draft76.prototype[key] = instance[key];
module.exports = Draft76;
+29
View File
@@ -0,0 +1,29 @@
var Headers = function() {
this._sent = {};
this._lines = [];
};
Headers.prototype.ALLOWED_DUPLICATES = ['set-cookie', 'set-cookie2', 'warning', 'www-authenticate']
Headers.prototype.set = function(name, value) {
if (value === undefined) return;
name = this._strip(name);
value = this._strip(value);
var key = name.toLowerCase();
if (!this._sent.hasOwnProperty(key) || this.ALLOWED_DUPLICATES.indexOf(key) < 0) {
this._sent[key] = true;
this._lines.push(name + ': ' + value + '\r\n');
}
};
Headers.prototype.toString = function() {
return this._lines.join('');
};
Headers.prototype._strip = function(string) {
return string.toString().replace(/^ */, '').replace(/ *$/, '');
};
module.exports = Headers;
+80
View File
@@ -0,0 +1,80 @@
var HTTPParser = process.binding('http_parser').HTTPParser,
version = HTTPParser.RESPONSE ? 6 : 4;
var HttpParser = function(type) {
if (type === 'request')
this._parser = new HTTPParser(HTTPParser.REQUEST || 'request');
else
this._parser = new HTTPParser(HTTPParser.RESPONSE || 'response');
this._type = type;
this._complete = false;
this.headers = {};
var current = null,
self = this;
this._parser.onHeaderField = function(b, start, length) {
current = b.toString('utf8', start, start + length).toLowerCase();
};
this._parser.onHeaderValue = function(b, start, length) {
self.headers[current] = b.toString('utf8', start, start + length);
};
this._parser.onHeadersComplete = this._parser[HTTPParser.kOnHeadersComplete] = function(info) {
self.method = (typeof info.method === 'number') ? HttpParser.METHODS[info.method] : info.method;
self.statusCode = info.statusCode;
self.url = info.url;
var headers = info.headers;
if (!headers) return;
for (var i = 0, n = headers.length; i < n; i += 2)
self.headers[headers[i].toLowerCase()] = headers[i+1];
};
this._parser.onMessageComplete = this._parser[HTTPParser.kOnMessageComplete] = function() {
self._complete = true;
};
};
HttpParser.METHODS = {
0: 'DELETE',
1: 'GET',
2: 'HEAD',
3: 'POST',
4: 'PUT',
5: 'CONNECT',
6: 'OPTIONS',
7: 'TRACE',
8: 'COPY',
9: 'LOCK',
10: 'MKCOL',
11: 'MOVE',
12: 'PROPFIND',
13: 'PROPPATCH',
14: 'SEARCH',
15: 'UNLOCK',
16: 'REPORT',
17: 'MKACTIVITY',
18: 'CHECKOUT',
19: 'MERGE',
24: 'PATCH'
};
HttpParser.prototype.isComplete = function() {
return this._complete;
};
HttpParser.prototype.parse = function(data) {
var offset = (version < 6) ? 1 : 0,
consumed = this._parser.execute(data, 0, data.length) + offset;
if (this._complete)
this.body = (consumed < data.length)
? data.slice(consumed)
: new Buffer(0);
};
module.exports = HttpParser;
+71 -53
View File
@@ -7,26 +7,33 @@ var Hybi = function(request, url, options) {
Base.apply(this, arguments);
this._reset();
this._reader = new Reader();
this._stage = 0;
this._masking = this._options.masking;
this._protocols = this._options.protocols || [];
this._reader = new Reader();
this._stage = 0;
this._masking = this._options.masking;
this._protocols = this._options.protocols || [];
this._requireMasking = this._options.requireMasking;
this._pingCallbacks = {};
if (typeof this._protocols === 'string')
this._protocols = this._protocols.split(/\s*,\s*/);
this._requireMasking = this._options.requireMasking;
this._pingCallbacks = {};
if (!this._request) return;
if (!this.version) {
var version = this._request.headers['sec-websocket-version'];
this.version = 'hybi-' + version;
var protos = this._request.headers['sec-websocket-protocol'],
supported = this._protocols;
if (protos !== undefined) {
if (typeof protos === 'string') protos = protos.split(/\s*,\s*/);
this.protocol = protos.filter(function(p) { return supported.indexOf(p) >= 0 })[0];
}
var version = this._request.headers['sec-websocket-version'];
this.version = 'hybi-' + version;
};
util.inherits(Hybi, Base);
Hybi.mask = function(payload, mask, offset) {
if (mask.length === 0) return payload;
if (!mask || mask.length === 0) return payload;
offset = offset || 0;
for (var i = 0, n = payload.length - offset; i < n; i++) {
@@ -66,7 +73,6 @@ var instance = {
FRAGMENTED_OPCODES: [0, 1, 2],
OPENING_OPCODES: [1, 2],
MAX_LENGTH: Math.pow(2, 53) - 1,
TWO_POWERS: [0, 1, 2, 3, 4, 5, 6, 7].map(function(n) { return Math.pow(2, 8 * n) }),
ERRORS: {
@@ -119,17 +125,19 @@ var instance = {
case 4:
buffer = this._reader.read(this._length);
if (buffer) {
this._payload = buffer;
this._emitFrame();
this._emitFrame(buffer);
this._stage = 0;
}
break;
default:
buffer = null;
}
}
},
frame: function(data, type, code) {
if (this.readyState === 0) return this._queue([data, type, code]);
if (this.readyState <= 0) return this._queue([data, type, code]);
if (this.readyState !== 1) return false;
if (data instanceof Array) data = new Buffer(data);
@@ -201,7 +209,7 @@ var instance = {
reason = reason || '';
code = code || this.ERRORS.normal_closure;
if (this.readyState === 0) {
if (this.readyState <= 0) {
this.readyState = 3;
this.emit('close', new Base.CloseEvent(code, reason));
return true;
@@ -218,33 +226,23 @@ var instance = {
var secKey = this._request.headers['sec-websocket-key'];
if (!secKey) return '';
var accept = Hybi.generateAccept(secKey),
protos = this._request.headers['sec-websocket-protocol'],
supported = this._protocols,
proto,
var headers = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
'Sec-WebSocket-Accept: ' + Hybi.generateAccept(secKey)
];
headers = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
'Sec-WebSocket-Accept: ' + accept
];
if (this.protocol)
headers.push('Sec-WebSocket-Protocol: ' + this.protocol);
if (protos !== undefined) {
if (typeof protos === 'string') protos = protos.split(/\s*,\s*/);
proto = protos.filter(function(p) { return supported.indexOf(p) >= 0 })[0];
if (proto) {
this.protocol = proto;
headers.push('Sec-WebSocket-Protocol: ' + proto);
}
}
return new Buffer(headers.concat('','').join('\r\n'), 'utf8');
return new Buffer(headers.concat(this.__headers.toString(), '').join('\r\n'), 'utf8');
},
_shutdown: function(code, reason) {
this.frame(reason, 'close', code);
this.readyState = 3;
this._stage = 5;
this.emit('close', new Base.CloseEvent(code, reason));
},
@@ -266,8 +264,6 @@ var instance = {
this._final = (data & this.FIN) === this.FIN;
this._opcode = (data & this.OPCODE);
this._mask = [];
this._payload = [];
if (this.OPCODE_CODES.indexOf(this._opcode) < 0)
return this._fail('protocol_error', 'Unrecognized frame opcode: ' + this._opcode);
@@ -289,11 +285,11 @@ var instance = {
this._length = (data & this.LENGTH);
if (this._length >= 0 && this._length <= 125) {
if (!this._checkFrameLength()) return;
this._stage = this._masked ? 3 : 4;
} else {
this._lengthBuffer = [];
this._lengthSize = (this._length === 126 ? 2 : 8);
this._stage = 2;
this._lengthSize = (this._length === 126 ? 2 : 8);
this._stage = 2;
}
},
@@ -303,21 +299,32 @@ var instance = {
if (this.FRAGMENTED_OPCODES.indexOf(this._opcode) < 0 && this._length > 125)
return this._fail('protocol_error', 'Received control frame having too long payload: ' + this._length);
if (this._length > this.MAX_LENGTH)
return this._fail('too_large', 'WebSocket frame length too large');
if (!this._checkFrameLength()) return;
this._stage = this._masked ? 3 : 4;
},
_emitFrame: function() {
var payload = Hybi.mask(this._payload, this._mask),
_checkFrameLength: function() {
if (this.__blength + this._length > this._maxLength) {
this._fail('too_large', 'WebSocket frame length too large');
return false;
} else {
return true;
}
},
_emitFrame: function(buffer) {
var payload = Hybi.mask(buffer, this._mask),
isFinal = this._final,
opcode = this._opcode;
this._final = this._opcode = this._length = this._lengthSize = this._masked = this._mask = null;
if (opcode === this.OPCODES.continuation) {
if (!this._mode) return this._fail('protocol_error', 'Received unexpected continuation frame');
this._buffer(payload);
if (this._final) {
var message = new Buffer(this.__buffer);
if (isFinal) {
var message = this._concatBuffer();
if (this._mode === 'text') message = this._encode(message);
this._reset();
if (message === null)
@@ -327,7 +334,7 @@ var instance = {
}
}
else if (opcode === this.OPCODES.text) {
if (this._final) {
if (isFinal) {
var message = this._encode(payload);
if (message === null)
this._fail('encoding_error', 'Could not decode a text frame as UTF-8');
@@ -339,7 +346,7 @@ var instance = {
}
}
else if (opcode === this.OPCODES.binary) {
if (this._final) {
if (isFinal) {
this.emit('message', new Base.MessageEvent(payload));
} else {
this._mode = 'binary';
@@ -374,13 +381,25 @@ var instance = {
},
_buffer: function(fragment) {
for (var i = 0, n = fragment.length; i < n; i++)
this.__buffer.push(fragment[i]);
this.__buffer.push(fragment);
this.__blength += fragment.length;
},
_concatBuffer: function() {
var buffer = new Buffer(this.__blength),
offset = 0;
for (var i = 0, n = this.__buffer.length; i < n; i++) {
this.__buffer[i].copy(buffer, offset);
offset += this.__buffer[i].length;
}
return buffer;
},
_reset: function() {
this._mode = null;
this.__buffer = [];
this._mode = null;
this.__buffer = [];
this.__blength = 0;
},
_encode: function(buffer) {
@@ -403,4 +422,3 @@ for (var key in instance)
Hybi.prototype[key] = instance[key];
module.exports = Hybi;
+17 -18
View File
@@ -1,41 +1,40 @@
var StreamReader = function() {
this._queue = [];
this._cursor = 0;
};
StreamReader.prototype.read = function(bytes) {
return this._readBuffer(bytes);
this._queue = [];
this._queueSize = 0;
this._cursor = 0;
};
StreamReader.prototype.put = function(buffer) {
if (!buffer || buffer.length === 0) return;
if (!buffer.copy) buffer = new Buffer(buffer);
this._queue.push(buffer);
this._queueSize += buffer.length;
};
StreamReader.prototype._readBuffer = function(length) {
StreamReader.prototype.read = function(length) {
if (length > this._queueSize) return null;
var buffer = new Buffer(length),
queue = this._queue,
remain = length,
n = queue.length,
i = 0,
chunk, offset, size;
if (remain === 0) return buffer;
chunk, size;
while (remain > 0 && i < n) {
chunk = queue[i];
offset = (i === 0) ? this._cursor : 0;
size = Math.min(remain, chunk.length - offset);
chunk.copy(buffer, length - remain, offset, offset + size);
remain -= size;
size = Math.min(remain, chunk.length - this._cursor);
chunk.copy(buffer, length - remain, this._cursor, this._cursor + size);
remain -= size;
this._queueSize -= size;
this._cursor = (this._cursor + size) % chunk.length;
i += 1;
}
if (remain > 0) return null;
queue.splice(0, i-1);
this._cursor = (i === 1 ? this._cursor : 0) + size;
queue.splice(0, this._cursor === 0 ? i : i - 1);
return buffer;
};
+106
View File
@@ -0,0 +1,106 @@
var util = require('util'),
HttpParser = require('./http_parser'),
Base = require('./base'),
Draft75 = require('./draft75'),
Draft76 = require('./draft76'),
Hybi = require('./hybi');
var Server = function(options) {
Base.call(this, null, null, options);
this._http = new HttpParser('request');
};
util.inherits(Server, Base);
var instance = {
EVENTS: ['open', 'message', 'error', 'close'],
_bindEventListeners: function() {
this.messages.on('error', function() {});
this.on('error', function() {});
},
parse: function(data) {
if (this._delegate) return this._delegate.parse(data);
this._http.parse(data);
if (!this._http.isComplete()) return;
this.method = this._http.method;
this.url = this._http.url;
this.headers = this._http.headers;
this.body = this._http.body;
var self = this;
this._delegate = Server.http(this, this._options);
this._delegate.messages = this.messages;
this._delegate.io = this.io;
this._delegate.on('open', function() { self._open() });
this.EVENTS.forEach(function(event) {
this._delegate.on(event, function(e) { self.emit(event, e) });
}, this);
this.protocol = this._delegate.protocol;
this.version = this._delegate.version;
this.parse(this._http.body);
this.emit('connect', new Base.ConnectEvent());
},
_open: function() {
this.__queue.forEach(function(msg) {
this._delegate[msg[0]].apply(this._delegate, msg[1]);
}, this);
this.__queue = [];
}
};
['setHeader', 'start', 'state', 'frame', 'text', 'binary', 'ping', 'close'].forEach(function(method) {
instance[method] = function() {
if (this._delegate) {
return this._delegate[method].apply(this._delegate, arguments);
} else {
this.__queue.push([method, arguments]);
return true;
}
};
});
for (var key in instance)
Server.prototype[key] = instance[key];
Server.isSecureRequest = function(request) {
if (request.connection && request.connection.authorized !== undefined) return true;
if (request.socket && request.socket.secure) return true;
var headers = request.headers;
if (!headers) return false;
if (headers['https'] === 'on') return true;
if (headers['x-forwarded-ssl'] === 'on') return true;
if (headers['x-forwarded-scheme'] === 'https') return true;
if (headers['x-forwarded-proto'] === 'https') return true;
return false;
};
Server.determineUrl = function(request) {
var scheme = this.isSecureRequest(request) ? 'wss:' : 'ws:';
return scheme + '//' + request.headers.host + request.url;
};
Server.http = function(request, options) {
options = options || {};
if (options.requireMasking === undefined) options.requireMasking = true;
var headers = request.headers,
url = this.determineUrl(request);
if (headers['sec-websocket-version'])
return new Hybi(request, url, options);
else if (headers['sec-websocket-key1'])
return new Draft76(request, url, options);
else
return new Draft75(request, url, options);
};
module.exports = Server;
-1
View File
@@ -141,4 +141,3 @@ Messages.prototype.destroy = function() {};
exports.IO = IO;
exports.Messages = Messages;
+8 -14
View File
@@ -3,24 +3,18 @@
, "homepage" : "http://github.com/faye/websocket-driver-node"
, "author" : "James Coglan <jcoglan@gmail.com> (http://jcoglan.com/)"
, "keywords" : ["websocket"]
, "license" : "MIT"
, "version" : "0.1.0"
, "version" : "0.3.6"
, "engines" : {"node": ">=0.4.0"}
, "main" : "./lib/websocket/driver"
, "devDependencies" : {"jsclass": ""}
, "devDependencies" : {"jstest": ""}
, "scripts" : {"test": "node spec/runner.js"}
, "scripts" : {"test": "jstest spec/runner.js"}
, "repository" : { "type" : "git"
, "url" : "git://github.com/faye/websocket-driver-node.git"
}
, "bugs" : "http://github.com/faye/websocket-driver-node/issues"
, "licenses" : [ { "type" : "MIT"
, "url" : "http://www.opensource.org/licenses/mit-license.php"
}
]
, "repositories" : [ { "type" : "git"
, "url" : "git://github.com/faye/websocket-driver-node.git"
}
]
}
+14 -19
View File
@@ -1,6 +1,5 @@
require('jsclass')
var Stream = require('stream').Stream,
var test = require('jstest').Test,
Stream = require('stream').Stream,
util = require('util')
var BufferMatcher = function(data) {
@@ -29,21 +28,17 @@ Collector.prototype.write = function(buffer) {
return true
}
JS.require('JS.Test', function() {
JS.Test.Unit.TestCase.include({
buffer: function(data) {
return new BufferMatcher(data)
},
collector: function() {
return this._collector = this._collector || new Collector()
}
})
require('./websocket/driver/draft75_examples')
require('./websocket/driver/draft75_spec')
require('./websocket/driver/draft76_spec')
require('./websocket/driver/hybi_spec')
require('./websocket/driver/client_spec')
JS.Test.autorun()
test.Unit.TestCase.include({
buffer: function(data) {
return new BufferMatcher(data)
},
collector: function() {
return this._collector = this._collector || new Collector()
}
})
require('./websocket/driver/draft75_examples')
require('./websocket/driver/draft75_spec')
require('./websocket/driver/draft76_spec')
require('./websocket/driver/hybi_spec')
require('./websocket/driver/client_spec')
+74 -4
View File
@@ -1,6 +1,7 @@
var Client = require("../../../lib/websocket/driver/client")
var Client = require("../../../lib/websocket/driver/client"),
test = require('jstest').Test
JS.Test.describe("Client", function() { with(this) {
test.describe("Client", function() { with(this) {
define("options", function() {
return this._options = this._options || {protocols: this.protocols()}
})
@@ -9,9 +10,13 @@ JS.Test.describe("Client", function() { with(this) {
null
})
define("url", function() {
return "ws://www.example.com/socket"
})
define("driver", function() {
if (this._driver) return this._driver
this._driver = new Client("ws://www.example.com/socket", this.options())
this._driver = new Client(this.url(), this.options())
var self = this
this._driver.on('open', function(e) { self.open = true })
this._driver.on('message', function(e) { self.message += e.data })
@@ -45,6 +50,14 @@ JS.Test.describe("Client", function() { with(this) {
assertEqual( null, driver().getState() )
}})
describe("close", function() { with(this) {
it("changes the state to closed", function() { with(this) {
driver().close()
assertEqual( "closed", driver().getState() )
assertEqual( [1000, ''], close )
}})
}})
describe("start", function() { with(this) {
it("writes the handshake request to the socket", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
@@ -79,6 +92,42 @@ JS.Test.describe("Client", function() { with(this) {
}})
}})
describe("with basic auth", function() { with(this) {
define("url", function() { return "ws://user:pass@www.example.com/socket" })
it("writes the handshake with Authorization", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"GET /socket HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" +
"Sec-WebSocket-Version: 13\r\n" +
"Authorization: Basic dXNlcjpwYXNz\r\n" +
"\r\n"))
driver().start()
}})
}})
describe("with custom headers", function() { with(this) {
before(function() { with(this) {
driver().setHeader("User-Agent", "Chrome")
}})
it("writes the handshake with custom headers", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"GET /socket HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" +
"Sec-WebSocket-Version: 13\r\n" +
"User-Agent: Chrome\r\n" +
"\r\n"))
driver().start()
}})
}})
it("changes the state to connecting", function() { with(this) {
driver().start()
assertEqual( "connecting", driver().getState() )
@@ -97,6 +146,14 @@ JS.Test.describe("Client", function() { with(this) {
assertEqual( false, close )
assertEqual( "open", driver().getState() )
}})
it("makes the response status available", function() { with(this) {
assertEqual( 101, driver().statusCode )
}})
it("makes the response headers available", function() { with(this) {
assertEqual( "websocket", driver().headers.upgrade )
}})
}})
describe("with a valid response followed by a frame", function() { with(this) {
@@ -118,6 +175,20 @@ JS.Test.describe("Client", function() { with(this) {
}})
}})
describe("with a bad status line", function() { with(this) {
before(function() {
var resp = this.response().replace(/101/g, "4")
this.driver().parse(new Buffer(resp))
})
it("changes the state to closed", function() { with(this) {
assertEqual( false, open )
assertEqual( "Error during WebSocket handshake: Unexpected response code: 4", error.message )
assertEqual( [1002, "Error during WebSocket handshake: Unexpected response code: 4"], close )
assertEqual( "closed", driver().getState() )
}})
}})
describe("with a bad Upgrade header", function() { with(this) {
before(function() {
var resp = this.response().replace(/websocket/g, "wrong")
@@ -186,4 +257,3 @@ JS.Test.describe("Client", function() { with(this) {
}})
}})
}})
+3 -2
View File
@@ -1,4 +1,6 @@
JS.Test.describe("draft-75", function() { with(this) {
var test = require('jstest').Test
test.describe("draft-75", function() { with(this) {
sharedExamplesFor("draft-75 protocol", function() { with(this) {
describe("in the open state", function() { with(this) {
before(function() { this.driver().start() })
@@ -110,4 +112,3 @@ JS.Test.describe("draft-75", function() { with(this) {
}})
}})
}})
+3 -3
View File
@@ -1,6 +1,7 @@
var Draft75 = require("../../../lib/websocket/driver/draft75")
var Draft75 = require("../../../lib/websocket/driver/draft75"),
test = require('jstest').Test
JS.Test.describe("Draft75", function() { with(this) {
test.describe("Draft75", function() { with(this) {
define("request", function() {
return this._request = this._request || {
headers: {
@@ -96,4 +97,3 @@ JS.Test.describe("Draft75", function() { with(this) {
itShouldBehaveLike("draft-75 protocol")
}})
+6 -6
View File
@@ -1,6 +1,7 @@
var Draft76 = require("../../../lib/websocket/driver/draft76")
var Draft76 = require("../../../lib/websocket/driver/draft76"),
test = require('jstest').Test
JS.Test.describe("Draft76", function() { with(this) {
test.describe("Draft76", function() { with(this) {
BODY = new Buffer([0x91, 0x25, 0x3e, 0xd3, 0xa9, 0xe7, 0x6a, 0x88])
define("body", function() {
@@ -52,7 +53,7 @@ JS.Test.describe("Draft76", function() { with(this) {
describe("start", function() { with(this) {
it("writes the handshake response to the socket", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"HTTP/1.1 101 Web Socket Protocol Handshake\r\n" +
"HTTP/1.1 101 WebSocket Protocol Handshake\r\n" +
"Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Origin: http://www.example.com\r\n" +
@@ -94,7 +95,7 @@ JS.Test.describe("Draft76", function() { with(this) {
it("queues the frames until the handshake has been sent", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"HTTP/1.1 101 Web Socket Protocol Handshake\r\n" +
"HTTP/1.1 101 WebSocket Protocol Handshake\r\n" +
"Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Origin: http://www.example.com\r\n" +
@@ -115,7 +116,7 @@ JS.Test.describe("Draft76", function() { with(this) {
it("writes the handshake response with no body", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"HTTP/1.1 101 Web Socket Protocol Handshake\r\n" +
"HTTP/1.1 101 WebSocket Protocol Handshake\r\n" +
"Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Origin: http://www.example.com\r\n" +
@@ -183,4 +184,3 @@ JS.Test.describe("Draft76", function() { with(this) {
}})
}})
}})
+21 -4
View File
@@ -1,6 +1,7 @@
var Hybi = require("../../../lib/websocket/driver/hybi")
var Hybi = require("../../../lib/websocket/driver/hybi"),
test = require('jstest').Test
JS.Test.describe("Hybi", function() { with(this) {
test.describe("Hybi", function() { with(this) {
define("request", function() {
return this._request = this._request || {
headers: {
@@ -78,6 +79,23 @@ JS.Test.describe("Hybi", function() { with(this) {
}})
}})
describe("with custom headers", function() { with(this) {
before(function() { with(this) {
driver().setHeader("Authorization", "Bearer WAT")
}})
it("writes the handshake with Sec-WebSocket-Protocol", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"HTTP/1.1 101 Switching Protocols\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" +
"Authorization: Bearer WAT\r\n" +
"\r\n"))
driver().start()
}})
}})
it("triggers the onopen event", function() { with(this) {
driver().start()
assertEqual( true, open )
@@ -290,7 +308,7 @@ JS.Test.describe("Hybi", function() { with(this) {
}})
it("returns an error for too-large frames", function() { with(this) {
driver().parse([0x81, 0x7f, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
driver().parse([0x81, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00])
assertEqual( "WebSocket frame length too large", error.message )
assertEqual( [1009, "WebSocket frame length too large"], close )
assertEqual( "closed", driver().getState() )
@@ -521,4 +539,3 @@ JS.Test.describe("Hybi", function() { with(this) {
}})
}})
}})