Compare commits

...

57 Commits

Author SHA1 Message Date
James Coglan e8992add23 Bump version to 0.5.0 and document how to add extensions. 2014-12-13 13:00:47 +00:00
James Coglan 23675e08ee Don't expose the internal frame structure of messages to extensions; they should not be able to depend on seeing the actual frame boundaries off the wire because that requires all extensions to leave them intact. 2014-12-06 22:08:37 +00:00
James Coglan 66f46330e7 Remove some blank lines. 2014-12-05 19:36:06 +00:00
James Coglan 7376f48d6a Catch errors raised by Extensions.activate(). 2014-12-02 22:53:16 +00:00
James Coglan 330fa2073d Don't assume that messages coming out of extensions have anything but the standard websocket-extensions API. 2014-11-29 01:54:20 +00:00
James Coglan 58474a837c Populate the Message.data field before handing off to extensions. 2014-11-29 01:13:18 +00:00
James Coglan 82182eb348 Close the Extensions object when ending the session. 2014-11-29 00:55:02 +00:00
James Coglan 94041e7a86 websocket-extensions uses Node-style callbacks rather than emitting error events. 2014-11-27 09:32:49 +00:00
James Coglan e73f7db754 Rename validFrame() to validFrameRsv(). 2014-11-26 09:59:08 +00:00
James Coglan 0efc0ebb8d Don't pass the driver into the Extensions constructor. 2014-11-25 23:17:39 +00:00
James Coglan f904131d21 Extract the Extensions class into another module. 2014-11-25 21:14:17 +00:00
James Coglan ee664ba6c9 Decouple Extensions and individual extension libs from the Driver.fail() method, emitting errors instead. 2014-11-25 19:42:54 +00:00
James Coglan 538239b761 Move the header-parsing functions into class methods on Extensions. 2014-11-25 09:11:58 +00:00
James Coglan 2bc103c781 Adjust the data structures in the Extensions parser so that the server sets the order of extensions, per the spec. 2014-11-24 23:03:50 +00:00
James Coglan eceb59e2fb Implement the client-side framework for extensions. 2014-11-24 22:05:31 +00:00
James Coglan 40341c5439 The Client driver requires the crypto module for generating keys. 2014-11-24 00:41:34 +00:00
James Coglan 396637d463 Extract permessage-deflate into a separate module. 2014-11-23 21:46:04 +00:00
James Coglan c986ad231c Enable strict mode on all source files. 2014-11-23 20:09:26 +00:00
James Coglan ef897eed1c Implement deflate compression on server-to-client messages. 2014-11-23 19:45:02 +00:00
James Coglan 724a5f19a4 Flesh out the parameter validation for extensions, and provide better support for multiple values of extension params and HTTP headers. 2014-11-23 15:21:11 +00:00
James Coglan 0332ce2625 Initial support for permessage-deflate, only supports client-to-server compression but enough to pass the Autobahn suite. 2014-11-23 13:11:24 +00:00
James Coglan 6146615801 Extract Hybi parser state into Frame and Message structs.
This makes the structure of all the state in the parser easier to see,
it makes it easier to free memory by deleting a single struct rather
than many related fields, and it produces some data structures that will
be useful for adding extensions.
2014-11-22 13:34:34 +00:00
James Coglan 22d483a5ee Update the 0.4.0 release date. 2014-11-08 19:43:57 +00:00
James Coglan 7325241d11 Move the header setting for Sec-WebSocket-Protocol. 2014-11-08 17:46:03 +00:00
James Coglan eb2d329a16 Clean up some formatting typos. 2014-11-08 17:20:51 +00:00
James Coglan 7e34e7fd40 Correct the name of the test about proxy 'connect' events. 2014-11-08 16:43:36 +00:00
James Coglan 623a93c0b7 Duplicate headers should be allowed if ALLOWED_DUPLICATES *includes* the name. 2014-11-08 16:39:40 +00:00
James Coglan 208be33754 Refactor the way headers are generated in all the drivers. 2014-11-08 16:12:14 +00:00
James Coglan 0bb47c17b4 Remove TLS concerns from the proxy implementation.
This results in a more simple design, in the sense of fewer concerns
being mixed together. It makes the API more verbose, but it's not
difficult to use correctly.

It also means that we make the I/O implementation deal with TLS, so it
can use tls.connect({socket: io}) or the upcoming TLSSocket class,
rather than this module needing a huge amount of boilerplate and
internal indirection to use tls.createSecurePair() and make Proxy wrap
pair.encrypted.

Using tls.connect({socket: io}) would be awkward on this end, because it
expects a stream that emits *incoming* data from the TCP connection. All
the streams in this module emit *outgoing* data that should be written
to TCP, so their polarity is flipped compared to what tls.connect()
wants, and resolving that involves even more stream boilerplate.
2014-11-08 15:20:08 +00:00
James Coglan d6da566641 Only call tls.checkServerIdentity() if it is available (i.e. before Node 0.8.). 2014-11-07 19:17:12 +00:00
James Coglan cde48d625c Move http_parser.js up into the lib/websocket directory. 2014-11-07 19:16:35 +00:00
James Coglan 6eb04d5acc Remove a redundant assignment. 2014-11-06 23:20:32 +00:00
James Coglan 4348bec390 Some cleanup to remove bits I left lying around while refactoring proxy support. 2014-11-06 23:04:11 +00:00
James Coglan 550b3d906d Unify the proxy handshake, TLS handshake, and WebSocket protocol into a single stream when using driver.proxy() to simplify the API. 2014-11-05 23:30:15 +00:00
James Coglan 473fcaafdb Merge pull request #9 from meteor/close-proxy
proxy should emit 'close' when it's done
2014-10-30 00:28:44 +00:00
David Glasser b88b8d6856 proxy should emit 'close' when it's done
This will cause any stream piped into it to stop piping into it. (It's
the only way to unpipe a Node 0.8 Streams1-style pipe.)

This change allows you to remove the empty "stream.on('data')" in
faye-websocket.
2014-10-29 16:47:56 -07:00
James Coglan b905c94f89 Make a TCP connection to the proxy in the proxy example. 2014-10-29 23:07:03 +00:00
James Coglan 7b47563cd1 Mark code up as JavaScript. 2014-10-29 22:38:29 +00:00
James Coglan 07031911e3 s/wail/will/ 2014-10-29 22:31:33 +00:00
James Coglan d2156ac10d Clarify that the 'tls' option is for use with wss: connections. 2014-10-29 22:19:40 +00:00
James Coglan d0033b8208 Change TLS values in the proxy docs. 2014-10-29 22:17:03 +00:00
James Coglan 76f4128634 Redesign the proxy API so that we can more cleanly deal with TLS, either to the proxy server or origin server or both, while still allowing control of the TLS options and custom headers, and not leaking so much of the implementation. 2014-10-29 22:07:01 +00:00
James Coglan 32e6715b6e [wip] Emit a proxy-connect event so the client can perform a TLS handshake at the right time. 2014-10-29 19:27:30 +00:00
James Coglan 042e33aebf Bump version to 0.4.0. 2014-10-27 09:35:13 +00:00
James Coglan bdc9bfe7e8 Add support for connecting via HTTP proxies. 2014-10-27 09:30:11 +00:00
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
25 changed files with 691 additions and 293 deletions
-1
View File
@@ -8,4 +8,3 @@ node_js:
before_install:
- '[ "${TRAVIS_NODE_VERSION}" = "0.6" ] && npm conf set strict-ssl false || true'
+21 -1
View File
@@ -1,3 +1,24 @@
### 0.5.0 / 2014-12-13
* Support protocol extensions via the websocket-extensions module
### 0.4.0 / 2014-11-08
* Support connection via HTTP proxies using `CONNECT`
### 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
@@ -35,4 +56,3 @@
### 0.1.0 / 2013-05-04
* First stable release
+75 -7
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
@@ -14,6 +14,9 @@ hook this module up to some I/O object, it will do all of this for you:
* Generate and send both server- and client-side handshakes
* Recognize when the handshake phase completes and the WS protocol begins
* Negotiate subprotocol selection based on `Sec-WebSocket-Protocol`
* Negotiate and use extensions via the
[websocket-extensions](https://github.com/faye/websocket-extensions-node)
module
* Buffer sent messages until the handshake process is finished
* Deal with proxies that defer delivery of the draft-76 handshake body
* Notify you when the socket is open and closed and when messages arrive
@@ -134,17 +137,17 @@ var net = require('net'),
websocket = require('websocket-driver');
var driver = websocket.client('ws://www.example.com/socket'),
tcp = net.createConnection(80, 'www.example.com');
tcp = net.connect(80, 'www.example.com');
tcp.pipe(driver.io).pipe(tcp);
driver.messages.on('data', function(message) {
console.log('Got a message', message);
});
tcp.on('connect', function() {
driver.start();
});
driver.messages.on('data', function(message) {
console.log('Got a message', message);
});
```
Client drivers have two additional properties for reading the HTTP data that
@@ -154,6 +157,65 @@ was sent back by the server:
* `driver.headers` - an object containing the response headers
### HTTP Proxies
The client driver supports connections via HTTP proxies using the `CONNECT`
method. Instead of sending the WebSocket handshake immediately, it will send a
`CONNECT` request, wait for a `200` response, and then proceed as normal.
To use this feature, call `driver.proxy(url)` where `url` is the origin of the
proxy, including a username and password if required. This produces a duplex
stream that you should pipe in and out of your TCP connection to the proxy
server. When the proxy emits `connect`, you can then pipe `driver.io` to your
TCP stream and call `driver.start()`.
```js
var net = require('net'),
websocket = require('websocket-driver');
var driver = websocket.client('ws://www.example.com/socket'),
proxy = driver.proxy('http://username:password@proxy.example.com'),
tcp = net.connect(80, 'proxy.example.com');
tcp.pipe(proxy).pipe(tcp, {end: false});
tcp.on('connect', function() {
proxy.start();
});
proxy.on('connect', function() {
driver.io.pipe(tcp).pipe(driver.io);
driver.start();
});
driver.messages.on('data', function(message) {
console.log('Got a message', message);
});
```
The proxy's `connect` event is also where you should perform a TLS handshake on
your TCP stream, if you are connecting to a `wss:` endpoint.
In the event that proxy connection fails, `proxy` will emit an `error`. You can
inspect the proxy's response via `proxy.statusCode` and `proxy.headers`.
```js
proxy.on('error', function(error) {
console.error(error.message);
console.log(proxy.statusCode);
console.log(proxy.headers);
});
```
Before calling `proxy.start()` you can set custom headers using
`proxy.setHeader()`:
```js
proxy.setHeader('User-Agent', 'node');
proxy.start();
```
### Driver API
Drivers are created using one of the following methods:
@@ -219,6 +281,13 @@ describing the error.
Sets the callback to execute when the socket becomes closed. The `event` object
has `code` and `reason` attributes.
#### `driver.addExtension(extension)`
Registers a protocol extension whose operation will be negotiated via the
`Sec-WebSocket-Extensions` header. `extension` is any extension compatible with
the [websocket-extensions](https://github.com/faye/websocket-extensions-node)
framework.
#### `driver.setHeader(name, value)`
Sets a custom header to be sent as part of the handshake response, either from
@@ -304,4 +373,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.
+3 -2
View File
@@ -1,8 +1,10 @@
var net = require('net'),
websocket = require('../lib/websocket/driver');
websocket = require('../lib/websocket/driver'),
deflate = require('permessage-deflate');
var server = net.createServer(function(connection) {
var driver = websocket.server();
driver.addExtension(deflate);
driver.on('connect', function() {
if (websocket.isWebSocket(driver)) driver.start();
@@ -18,4 +20,3 @@ var server = net.createServer(function(connection) {
});
server.listen(process.argv[2]);
+3 -2
View File
@@ -1,3 +1,5 @@
'use strict';
// Protocol references:
//
// * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
@@ -35,10 +37,9 @@ var Driver = {
upgrade = request.headers.upgrade || '';
return request.method === 'GET' &&
connection.toLowerCase().split(/\s*,\s*/).indexOf('upgrade') >= 0 &&
connection.toLowerCase().split(/ *, */).indexOf('upgrade') >= 0 &&
upgrade.toLowerCase() === 'websocket';
}
};
module.exports = Driver;
+8 -3
View File
@@ -1,3 +1,5 @@
'use strict';
var Emitter = require('events').EventEmitter,
util = require('util'),
streams = require('../streams'),
@@ -9,7 +11,7 @@ var Base = function(request, url, options) {
this._request = request;
this._options = options || {};
this._maxLength = this._options.maxLength || this.MAX_LENGTH;
this.__headers = new Headers();
this._headers = new Headers();
this.__queue = [];
this.readyState = 0;
this.url = url;
@@ -55,9 +57,13 @@ var instance = {
return this.STATES[this.readyState] || null;
},
addExtension: function(extension) {
return false;
},
setHeader: function(name, value) {
if (this.readyState > 0) return false;
this.__headers.set(name, value);
this._headers.set(name, value);
return true;
},
@@ -123,4 +129,3 @@ Base.MessageEvent = function(data) {
};
module.exports = Base;
+48 -28
View File
@@ -1,27 +1,50 @@
var url = require('url'),
util = require('util'),
HttpParser = require('./http_parser'),
Base = require('./base'),
Hybi = require('./hybi');
'use strict';
var Client = function(url, options) {
var crypto = require('crypto'),
url = require('url'),
util = require('util'),
HttpParser = require('../http_parser'),
Base = require('./base'),
Hybi = require('./hybi'),
Proxy = require('./proxy');
var Client = function(_url, options) {
this.version = 'hybi-13';
Hybi.call(this, null, url, options);
Hybi.call(this, null, _url, options);
this.readyState = -1;
this._key = Client.generateKey();
this._accept = Hybi.generateAccept(this._key);
this._http = new HttpParser('response');
var uri = url.parse(this.url),
auth = uri.auth && new Buffer(uri.auth, 'utf8').toString('base64');
this._pathname = (uri.pathname || '/') + (uri.search || '');
this._headers.set('Host', uri.host);
this._headers.set('Upgrade', 'websocket');
this._headers.set('Connection', 'Upgrade');
this._headers.set('Sec-WebSocket-Key', this._key);
this._headers.set('Sec-WebSocket-Version', '13');
if (this._protocols.length > 0)
this._headers.set('Sec-WebSocket-Protocol', this._protocols.join(', '));
if (auth)
this._headers.set('Authorization', 'Basic ' + auth);
};
util.inherits(Client, Hybi);
Client.generateKey = function() {
var buffer = new Buffer(16), i = buffer.length;
while (i--) buffer[i] = Math.floor(Math.random() * 256);
return buffer.toString('base64');
return crypto.randomBytes(16).toString('base64');
};
var instance = {
proxy: function(origin, options) {
return new Proxy(this, origin, options);
},
start: function() {
if (this.readyState !== -1) return false;
this._write(this._handshakeRequest());
@@ -34,29 +57,23 @@ var instance = {
this._http.parse(data);
if (!this._http.isComplete()) return;
this._validateHandshake();
if (this.readyState === 3) return;
this._open();
this.parse(this._http.body);
},
_handshakeRequest: function() {
var uri = url.parse(this.url);
var extensions = this._extensions.generateOffer();
if (extensions)
this._headers.set('Sec-WebSocket-Extensions', extensions);
var headers = [ 'GET ' + (uri.pathname || '/') + (uri.search || '') + ' HTTP/1.1',
'Host: ' + uri.hostname + (uri.port ? ':' + uri.port : ''),
'Upgrade: websocket',
'Connection: Upgrade',
'Sec-WebSocket-Key: ' + this._key,
'Sec-WebSocket-Version: 13'
];
var start = 'GET ' + this._pathname + ' HTTP/1.1',
headers = [start, this._headers.toString(), ''];
if (this._protocols.length > 0)
headers.push('Sec-WebSocket-Protocol: ' + this._protocols.join(', '));
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');
return new Buffer(headers.join('\r\n'), 'utf8');
},
_failHandshake: function(message) {
@@ -101,7 +118,11 @@ var instance = {
this.protocol = protocol;
}
this._open();
try {
this._extensions.activate(this.headers['sec-websocket-extensions']);
} catch (e) {
return this._failHandshake(e.message);
}
}
};
@@ -109,4 +130,3 @@ for (var key in instance)
Client.prototype[key] = instance[key];
module.exports = Client;
+11 -9
View File
@@ -1,3 +1,5 @@
'use strict';
var Base = require('./base'),
util = require('util');
@@ -5,6 +7,11 @@ var Draft75 = function(request, url, options) {
Base.apply(this, arguments);
this._stage = 0;
this.version = 'hixie-75';
this._headers.set('Upgrade', 'WebSocket');
this._headers.set('Connection', 'Upgrade');
this._headers.set('WebSocket-Origin', this._request.headers.origin);
this._headers.set('WebSocket-Location', this.url);
};
util.inherits(Draft75, Base);
@@ -88,14 +95,10 @@ var instance = {
},
_handshakeResponse: function() {
return new Buffer('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' +
'WebSocket-Origin: ' + this._request.headers.origin + '\r\n' +
'WebSocket-Location: ' + this.url + '\r\n' +
this.__headers.toString() +
'\r\n',
'utf8');
var start = 'HTTP/1.1 101 Web Socket Protocol Handshake',
headers = [start, this._headers.toString(), ''];
return new Buffer(headers.join('\r\n'), 'utf8');
},
_parseLeadingByte: function(data) {
@@ -115,4 +118,3 @@ for (var key in instance)
Draft75.prototype[key] = instance[key];
module.exports = Draft75;
+13 -12
View File
@@ -1,3 +1,5 @@
'use strict';
var Base = require('./base'),
Draft75 = require('./draft75'),
crypto = require('crypto'),
@@ -26,6 +28,13 @@ var Draft76 = function(request, url, options) {
this._stage = -1;
this._body = [];
this.version = 'hixie-76';
this._headers.clear();
this._headers.set('Upgrade', 'WebSocket');
this._headers.set('Connection', 'Upgrade');
this._headers.set('Sec-WebSocket-Origin', this._request.headers.origin);
this._headers.set('Sec-WebSocket-Location', this.url);
};
util.inherits(Draft76, Draft75);
@@ -48,14 +57,10 @@ var instance = {
},
_handshakeResponse: function() {
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');
var start = 'HTTP/1.1 101 WebSocket Protocol Handshake',
headers = [start, this._headers.toString(), ''];
return new Buffer(headers.join('\r\n'), 'binary');
},
_handshakeSignature: function() {
@@ -63,13 +68,10 @@ var instance = {
var body = new Buffer(this._body.slice(0, this.BODY_SIZE));
var headers = this._request.headers,
key1 = headers['sec-websocket-key1'],
value1 = numberFromKey(key1) / spacesInKey(key1),
key2 = headers['sec-websocket-key2'],
value2 = numberFromKey(key2) / spacesInKey(key2),
md5 = crypto.createHash('md5');
md5.update(bigEndian(value1));
@@ -106,4 +108,3 @@ for (var key in instance)
Draft76.prototype[key] = instance[key];
module.exports = Draft76;
+9 -4
View File
@@ -1,10 +1,16 @@
'use strict';
var Headers = function() {
this.clear();
};
Headers.prototype.ALLOWED_DUPLICATES = ['set-cookie', 'set-cookie2', 'warning', 'www-authenticate'];
Headers.prototype.clear = 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;
@@ -12,7 +18,7 @@ Headers.prototype.set = function(name, value) {
value = this._strip(value);
var key = name.toLowerCase();
if (!this._sent.hasOwnProperty(key) || this.ALLOWED_DUPLICATES.indexOf(key) < 0) {
if (!this._sent.hasOwnProperty(key) || this.ALLOWED_DUPLICATES.indexOf(key) >= 0) {
this._sent[key] = true;
this._lines.push(name + ': ' + value + '\r\n');
}
@@ -27,4 +33,3 @@ Headers.prototype._strip = function(string) {
};
module.exports = Headers;
+225 -187
View File
@@ -1,32 +1,50 @@
var crypto = require('crypto'),
util = require('util'),
Base = require('./base'),
Reader = require('./hybi/stream_reader');
'use strict';
var crypto = require('crypto'),
util = require('util'),
Extensions = require('websocket-extensions'),
Base = require('./base'),
Frame = require('./hybi/frame'),
Message = require('./hybi/message'),
Reader = require('./hybi/stream_reader');
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 || [];
if (typeof this._protocols === 'string')
this._protocols = this._protocols.split(/\s*,\s*/);
this._extensions = new Extensions();
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 (!this.version) {
var version = this._request.headers['sec-websocket-version'];
this.version = 'hybi-' + version;
if (typeof this._protocols === 'string')
this._protocols = this._protocols.split(/ *, */);
if (!this._request) return;
var secKey = this._request.headers['sec-websocket-key'],
protos = this._request.headers['sec-websocket-protocol'],
version = this._request.headers['sec-websocket-version'],
supported = this._protocols;
this._headers.set('Upgrade', 'websocket');
this._headers.set('Connection', 'Upgrade');
this._headers.set('Sec-WebSocket-Accept', Hybi.generateAccept(secKey));
if (protos !== undefined) {
if (typeof protos === 'string') protos = protos.split(/ *, */);
this.protocol = protos.filter(function(p) { return supported.indexOf(p) >= 0 })[0];
if (this.protocol) this._headers.set('Sec-WebSocket-Protocol', this.protocol);
}
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++) {
@@ -62,9 +80,9 @@ var instance = {
pong: 10
},
OPCODE_CODES: [0, 1, 2, 8, 9, 10],
FRAGMENTED_OPCODES: [0, 1, 2],
OPENING_OPCODES: [1, 2],
OPCODE_CODES: [0, 1, 2, 8, 9, 10],
MESSAGE_OPCODES: [0, 1, 2],
OPENING_OPCODES: [1, 2],
TWO_POWERS: [0, 1, 2, 3, 4, 5, 6, 7].map(function(n) { return Math.pow(2, 8 * n) }),
@@ -87,6 +105,11 @@ var instance = {
// http://www.w3.org/International/questions/qa-forms-utf-8.en.php
UTF8_MATCH: /^([\x00-\x7F]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})*$/,
addExtension: function(extension) {
this._extensions.add(extension);
return true;
},
parse: function(data) {
this._reader.put(data);
var buffer = true;
@@ -103,23 +126,22 @@ var instance = {
break;
case 2:
buffer = this._reader.read(this._lengthSize);
buffer = this._reader.read(this._frame.lengthBytes);
if (buffer) this._parseExtendedLength(buffer);
break;
case 3:
buffer = this._reader.read(4);
if (buffer) {
this._mask = buffer;
this._frame.maskingKey = buffer;
this._stage = 4;
}
break;
case 4:
buffer = this._reader.read(this._length);
buffer = this._reader.read(this._frame.length);
if (buffer) {
this._payload = buffer;
this._emitFrame();
this._emitFrame(buffer);
this._stage = 0;
}
break;
@@ -130,61 +152,6 @@ var instance = {
}
},
frame: function(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);
var isText = (typeof data === 'string'),
opcode = this.OPCODES[type || (isText ? 'text' : 'binary')],
buffer = isText ? new Buffer(data, 'utf8') : data,
insert = code ? 2 : 0,
length = buffer.length + insert,
header = (length <= 125) ? 2 : (length <= 65535 ? 4 : 10),
offset = header + (this._masking ? 4 : 0),
masked = this._masking ? this.MASK : 0,
frame = new Buffer(length + offset),
BYTE = this.BYTE,
mask, i;
frame[0] = this.FIN | opcode;
if (length <= 125) {
frame[1] = masked | length;
} else if (length <= 65535) {
frame[1] = masked | 126;
frame[2] = Math.floor(length / 256);
frame[3] = length & BYTE;
} else {
frame[1] = masked | 127;
frame[2] = Math.floor(length / Math.pow(2,56)) & BYTE;
frame[3] = Math.floor(length / Math.pow(2,48)) & BYTE;
frame[4] = Math.floor(length / Math.pow(2,40)) & BYTE;
frame[5] = Math.floor(length / Math.pow(2,32)) & BYTE;
frame[6] = Math.floor(length / Math.pow(2,24)) & BYTE;
frame[7] = Math.floor(length / Math.pow(2,16)) & BYTE;
frame[8] = Math.floor(length / Math.pow(2,8)) & BYTE;
frame[9] = length & BYTE;
}
if (code) {
frame[offset] = Math.floor(code / 256) & BYTE;
frame[offset+1] = code & BYTE;
}
buffer.copy(frame, offset + insert);
if (this._masking) {
mask = [Math.floor(Math.random() * 256), Math.floor(Math.random() * 256),
Math.floor(Math.random() * 256), Math.floor(Math.random() * 256)];
new Buffer(mask).copy(frame, header);
Hybi.mask(frame, mask, offset);
}
this._write(frame);
return true;
},
text: function(message) {
return this.frame(message, 'text');
},
@@ -203,7 +170,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;
@@ -216,39 +183,118 @@ var instance = {
}
},
_handshakeResponse: function() {
var secKey = this._request.headers['sec-websocket-key'];
if (!secKey) return '';
frame: function(data, type, code) {
if (this.readyState <= 0) return this._queue([data, type, code]);
if (this.readyState !== 1) return false;
var accept = Hybi.generateAccept(secKey),
protos = this._request.headers['sec-websocket-protocol'],
supported = this._protocols,
proto,
if (data instanceof Array) data = new Buffer(data);
headers = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
'Sec-WebSocket-Accept: ' + accept
];
var message = new Message(),
isText = (typeof data === 'string'),
payload, buffer;
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);
}
message.rsv1 = message.rsv2 = message.rsv3 = false;
message.opcode = this.OPCODES[type || (isText ? 'text' : 'binary')];
payload = isText ? new Buffer(data, 'utf8') : data;
if (code) {
buffer = payload;
payload = new Buffer(2 + buffer.length);
payload[0] = ~~(code / 256) & this.BYTE;
payload[1] = code & this.BYTE;
buffer.copy(payload, 2);
}
message.data = payload;
var onMessageReady = function(message) {
var frame = new Frame();
frame.final = true;
frame.rsv1 = message.rsv1;
frame.rsv2 = message.rsv2;
frame.rsv3 = message.rsv3;
frame.opcode = message.opcode;
frame.masked = !!this._masking;
frame.length = message.data.length;
frame.payload = message.data;
if (frame.masked) frame.maskingKey = crypto.randomBytes(4);
this._sendFrame(frame);
};
if (this.MESSAGE_OPCODES.indexOf(message.opcode) >= 0)
this._extensions.processOutgoingMessage(message, function(error, message) {
if (error) return this._fail('extension_error', error.message);
onMessageReady.call(this, message);
}, this);
else
onMessageReady.call(this, message);
return true;
},
_sendFrame: function(frame) {
var length = frame.length,
header = (length <= 125) ? 2 : (length <= 65535 ? 4 : 10),
offset = header + (frame.masked ? 4 : 0),
buffer = new Buffer(offset + length),
BYTE = this.BYTE,
masked = frame.masked ? this.MASK : 0;
buffer[0] = (frame.final ? this.FIN : 0) |
(frame.rsv1 ? this.RSV1 : 0) |
(frame.rsv2 ? this.RSV2 : 0) |
(frame.rsv3 ? this.RSV3 : 0) |
frame.opcode;
if (length <= 125) {
buffer[1] = masked | length;
} else if (length <= 65535) {
buffer[1] = masked | 126;
buffer[2] = ~~(length / 256);
buffer[3] = length & BYTE;
} else {
buffer[1] = masked | 127;
buffer[2] = ~~(length / Math.pow(2, 56)) & BYTE;
buffer[3] = ~~(length / Math.pow(2, 48)) & BYTE;
buffer[4] = ~~(length / Math.pow(2, 40)) & BYTE;
buffer[5] = ~~(length / Math.pow(2, 32)) & BYTE;
buffer[6] = ~~(length / Math.pow(2, 24)) & BYTE;
buffer[7] = ~~(length / Math.pow(2, 16)) & BYTE;
buffer[8] = ~~(length / Math.pow(2, 8)) & BYTE;
buffer[9] = length & BYTE;
}
return new Buffer(headers.concat(this.__headers.toString(), '').join('\r\n'), 'utf8');
if (frame.masked) {
frame.maskingKey.copy(buffer, header);
Hybi.mask(frame.payload, frame.maskingKey).copy(buffer, offset);
} else {
frame.payload.copy(buffer, offset);
}
this._write(buffer);
},
_handshakeResponse: function() {
var extensions = this._extensions.generateResponse(this._request.headers['sec-websocket-extensions']);
if (extensions) this._headers.set('Sec-WebSocket-Extensions', extensions);
var start = 'HTTP/1.1 101 Switching Protocols',
headers = [start, this._headers.toString(), ''];
return new Buffer(headers.join('\r\n'), 'utf8');
},
_shutdown: function(code, reason) {
this.frame(reason, 'close', code);
delete this._frame;
delete this._message;
this.readyState = 3;
this._stage = 5;
this.emit('close', new Base.CloseEvent(code, reason));
this._extensions.close();
},
_fail: function(type, message) {
@@ -261,58 +307,66 @@ var instance = {
return (data & rsv) === rsv;
});
if (rsvs.filter(function(rsv) { return rsv }).length > 0)
var frame = this._frame = new Frame();
frame.final = (data & this.FIN) === this.FIN;
frame.rsv1 = rsvs[0];
frame.rsv2 = rsvs[1];
frame.rsv3 = rsvs[2];
frame.opcode = (data & this.OPCODE);
if (!this._extensions.validFrameRsv(frame))
return this._fail('protocol_error',
'One or more reserved bits are on: reserved1 = ' + (rsvs[0] ? 1 : 0) +
', reserved2 = ' + (rsvs[1] ? 1 : 0) +
', reserved3 = ' + (rsvs[2] ? 1 : 0));
'One or more reserved bits are on: reserved1 = ' + (frame.rsv1 ? 1 : 0) +
', reserved2 = ' + (frame.rsv2 ? 1 : 0) +
', reserved3 = ' + (frame.rsv3 ? 1 : 0));
this._final = (data & this.FIN) === this.FIN;
this._opcode = (data & this.OPCODE);
this._mask = [];
this._payload = [];
if (this.OPCODE_CODES.indexOf(frame.opcode) < 0)
return this._fail('protocol_error', 'Unrecognized frame opcode: ' + frame.opcode);
if (this.OPCODE_CODES.indexOf(this._opcode) < 0)
return this._fail('protocol_error', 'Unrecognized frame opcode: ' + this._opcode);
if (this.MESSAGE_OPCODES.indexOf(frame.opcode) < 0 && !frame.final)
return this._fail('protocol_error', 'Received fragmented control frame: opcode = ' + frame.opcode);
if (this.FRAGMENTED_OPCODES.indexOf(this._opcode) < 0 && !this._final)
return this._fail('protocol_error', 'Received fragmented control frame: opcode = ' + this._opcode);
if (this._mode && this.OPENING_OPCODES.indexOf(this._opcode) >= 0)
if (this._message && this.OPENING_OPCODES.indexOf(frame.opcode) >= 0)
return this._fail('protocol_error', 'Received new data frame but previous continuous frame is unfinished');
this._stage = 1;
},
_parseLength: function(data) {
this._masked = (data & this.MASK) === this.MASK;
if (this._requireMasking && !this._masked)
var frame = this._frame;
frame.masked = (data & this.MASK) === this.MASK;
if (this._requireMasking && !frame.masked)
return this._fail('unacceptable', 'Received unmasked frame but masking is required');
this._length = (data & this.LENGTH);
frame.length = (data & this.LENGTH);
if (this._length >= 0 && this._length <= 125) {
if (frame.length >= 0 && frame.length <= 125) {
if (!this._checkFrameLength()) return;
this._stage = this._masked ? 3 : 4;
this._stage = frame.masked ? 3 : 4;
} else {
this._lengthSize = (this._length === 126 ? 2 : 8);
this._stage = 2;
frame.lengthBytes = (frame.length === 126 ? 2 : 8);
this._stage = 2;
}
},
_parseExtendedLength: function(buffer) {
this._length = this._getInteger(buffer);
var frame = this._frame;
frame.length = this._getInteger(buffer);
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.MESSAGE_OPCODES.indexOf(frame.opcode) < 0 && frame.length > 125)
return this._fail('protocol_error', 'Received control frame having too long payload: ' + frame.length);
if (!this._checkFrameLength()) return;
this._stage = this._masked ? 3 : 4;
this._stage = frame.masked ? 3 : 4;
},
_checkFrameLength: function() {
if (this.__blength + this._length > this._maxLength) {
var length = this._message ? this._message.length : 0;
if (length + this._frame.length > this._maxLength) {
this._fail('too_large', 'WebSocket frame length too large');
return false;
} else {
@@ -320,46 +374,32 @@ var instance = {
}
},
_emitFrame: function() {
var payload = Hybi.mask(this._payload, this._mask),
opcode = this._opcode;
_emitFrame: function(buffer) {
var frame = this._frame,
payload = frame.payload = Hybi.mask(buffer, frame.maskingKey),
opcode = frame.opcode,
message,
code, reason,
callbacks, callback;
delete this._frame;
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 = this._concatBuffer();
if (this._mode === 'text') message = this._encode(message);
this._reset();
if (message === null)
this._fail('encoding_error', 'Could not decode a text frame as UTF-8');
else
this.emit('message', new Base.MessageEvent(message));
}
if (!this._message) return this._fail('protocol_error', 'Received unexpected continuation frame');
this._message.pushFrame(frame);
}
else if (opcode === this.OPCODES.text) {
if (this._final) {
var message = this._encode(payload);
if (message === null)
this._fail('encoding_error', 'Could not decode a text frame as UTF-8');
else
this.emit('message', new Base.MessageEvent(message));
} else {
this._mode = 'text';
this._buffer(payload);
}
if (opcode === this.OPCODES.text || opcode === this.OPCODES.binary) {
this._message = new Message();
this._message.pushFrame(frame);
}
else if (opcode === this.OPCODES.binary) {
if (this._final) {
this.emit('message', new Base.MessageEvent(payload));
} else {
this._mode = 'binary';
this._buffer(payload);
}
}
else if (opcode === this.OPCODES.close) {
var code = (payload.length >= 2) ? 256 * payload[0] + payload[1] : null,
reason = (payload.length > 2) ? this._encode(payload.slice(2)) : null;
if (frame.final && this.MESSAGE_OPCODES.indexOf(opcode) >= 0)
return this._emitMessage(this._message);
if (opcode === this.OPCODES.close) {
code = (payload.length >= 2) ? 256 * payload[0] + payload[1] : null;
reason = (payload.length > 2) ? this._encode(payload.slice(2)) : null;
if (!(payload.length === 0) &&
!(code !== null && code >= this.MIN_RESERVED_ERROR && code <= this.MAX_RESERVED_ERROR) &&
@@ -371,39 +411,38 @@ var instance = {
this._shutdown(code, reason || '');
}
else if (opcode === this.OPCODES.ping) {
if (opcode === this.OPCODES.ping) {
this.frame(payload, 'pong');
}
else if (opcode === this.OPCODES.pong) {
var callbacks = this._pingCallbacks,
message = this._encode(payload),
callback = callbacks[message];
if (opcode === this.OPCODES.pong) {
callbacks = this._pingCallbacks;
message = this._encode(payload);
callback = callbacks[message];
delete callbacks[message];
if (callback) callback()
}
},
_buffer: function(fragment) {
this.__buffer.push(fragment);
this.__blength += fragment.length;
},
_emitMessage: function(message) {
var message = this._message;
message.read();
_concatBuffer: function() {
var buffer = new Buffer(this.__blength),
offset = 0;
delete this._message;
for (var i = 0, n = this.__buffer.length; i < n; i++) {
this.__buffer[i].copy(buffer, offset);
offset += this.__buffer[i].length;
}
return buffer;
},
this._extensions.processIncomingMessage(message, function(error, message) {
if (error) return this._fail('extension_error', error.message);
_reset: function() {
this._mode = null;
this.__buffer = [];
this.__blength = 0;
var payload = message.data;
if (message.opcode === this.OPCODES.text) payload = this._encode(payload);
if (payload === null)
return this._fail('encoding_error', 'Could not decode a text frame as UTF-8');
else
this.emit('message', new Base.MessageEvent(payload));
}, this);
},
_encode: function(buffer) {
@@ -426,4 +465,3 @@ for (var key in instance)
Hybi.prototype[key] = instance[key];
module.exports = Hybi;
+21
View File
@@ -0,0 +1,21 @@
'use strict';
var Frame = function() {};
var instance = {
final: false,
rsv1: false,
rsv2: false,
rsv3: false,
opcode: null,
masked: false,
maskingKey: null,
lengthBytes: 1,
length: 0,
payload: null
};
for (var key in instance)
Frame.prototype[key] = instance[key];
module.exports = Frame;
+41
View File
@@ -0,0 +1,41 @@
'use strict';
var Message = function() {
this.rsv1 = false;
this.rsv2 = false;
this.rsv3 = false;
this.opcode = null
this.length = 0;
this._chunks = [];
};
var instance = {
read: function() {
if (this.data) return this.data;
this.data = new Buffer(this.length);
var offset = 0;
for (var i = 0, n = this._chunks.length; i < n; i++) {
this._chunks[i].copy(this.data, offset);
offset += this._chunks[i].length;
}
return this.data;
},
pushFrame: function(frame) {
this.rsv1 = this.rsv1 || frame.rsv1;
this.rsv2 = this.rsv2 || frame.rsv2;
this.rsv3 = this.rsv3 || frame.rsv3;
if (this.opcode === null) this.opcode = frame.opcode;
this._chunks.push(frame.payload);
this.length += frame.length;
}
};
for (var key in instance)
Message.prototype[key] = instance[key];
module.exports = Message;
+12 -14
View File
@@ -1,13 +1,11 @@
'use strict';
var StreamReader = function() {
this._queue = [];
this._queueSize = 0;
this._cursor = 0;
};
StreamReader.prototype.read = function(bytes) {
return this._readBuffer(bytes);
};
StreamReader.prototype.put = function(buffer) {
if (!buffer || buffer.length === 0) return;
if (!buffer.copy) buffer = new Buffer(buffer);
@@ -15,7 +13,7 @@ StreamReader.prototype.put = function(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),
@@ -23,22 +21,22 @@ StreamReader.prototype._readBuffer = function(length) {
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;
}
queue.splice(0, i-1);
this._cursor = (i === 1 ? this._cursor : 0) + size;
queue.splice(0, this._cursor === 0 ? i : i - 1);
return buffer;
};
+97
View File
@@ -0,0 +1,97 @@
'use strict';
var Stream = require('stream').Stream,
url = require('url'),
util = require('util'),
Headers = require('./headers'),
HttpParser = require('../http_parser');
var PORTS = {'ws:': 80, 'wss:': 443};
var Proxy = function(client, origin, options) {
this._client = client;
this._http = new HttpParser('response');
this._origin = (typeof client.url === 'object') ? client.url : url.parse(client.url);
this._url = (typeof origin === 'object') ? origin : url.parse(origin);
this._options = options || {};
this._state = 0;
this.readable = this.writable = true;
this._paused = false;
this._headers = new Headers();
this._headers.set('Host', this._origin.host);
this._headers.set('Connection', 'keep-alive');
this._headers.set('Proxy-Connection', 'keep-alive');
var auth = this._url.auth && new Buffer(this._url.auth, 'utf8').toString('base64');
if (auth) this._headers.set('Proxy-Authorization', 'Basic ' + auth);
};
util.inherits(Proxy, Stream);
var instance = {
setHeader: function(name, value) {
if (this._state !== 0) return false;
this._headers.set(name, value);
return true;
},
start: function() {
if (this._state !== 0) return false;
this._state = 1;
var origin = this._origin,
port = origin.port || PORTS[origin.protocol],
start = 'CONNECT ' + origin.hostname + ':' + port + ' HTTP/1.1';
var headers = [start, this._headers.toString(), ''];
this.emit('data', new Buffer(headers.join('\r\n'), 'utf8'));
return true;
},
pause: function() {
this._paused = true;
},
resume: function() {
this._paused = false;
this.emit('drain');
},
write: function(chunk) {
if (!this.writable) return false;
this._http.parse(chunk);
if (!this._http.isComplete()) return !this._paused;
this.statusCode = this._http.statusCode;
this.headers = this._http.headers;
if (this.statusCode === 200) {
this.emit('connect');
} else {
var message = "Can't establish a connection to the server at " + this._origin.href;
this.emit('error', new Error(message));
}
this.end();
return !this._paused;
},
end: function(chunk) {
if (!this.writable) return;
if (chunk !== undefined) this.write(chunk);
this.readable = this.writable = false;
this.emit('close');
this.emit('end');
},
destroy: function() {
this.end();
}
};
for (var key in instance)
Proxy.prototype[key] = instance[key];
module.exports = Proxy;
+8 -4
View File
@@ -1,5 +1,7 @@
'use strict';
var util = require('util'),
HttpParser = require('./http_parser'),
HttpParser = require('../http_parser'),
Base = require('./base'),
Draft75 = require('./draft75'),
Draft76 = require('./draft76'),
@@ -34,12 +36,15 @@ var instance = {
this._delegate = Server.http(this, this._options);
this._delegate.messages = this.messages;
this._delegate.io = this.io;
this._open();
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());
},
@@ -52,7 +57,7 @@ var instance = {
}
};
['setHeader', 'start', 'state', 'frame', 'text', 'binary', 'ping', 'close'].forEach(function(method) {
['addExtension', 'setHeader', 'start', 'frame', 'text', 'binary', 'ping', 'close'].forEach(function(method) {
instance[method] = function() {
if (this._delegate) {
return this._delegate[method].apply(this._delegate, arguments);
@@ -101,4 +106,3 @@ Server.http = function(request, options) {
};
module.exports = Server;
@@ -1,3 +1,5 @@
'use strict';
var HTTPParser = process.binding('http_parser').HTTPParser,
version = HTTPParser.RESPONSE ? 6 : 4;
@@ -19,22 +21,31 @@ var HttpParser = function(type) {
};
this._parser.onHeaderValue = function(b, start, length) {
self.headers[current] = b.toString('utf8', start, start + length);
var value = b.toString('utf8', start, start + length);
if (self.headers.hasOwnProperty(current))
self.headers[current] += ', ' + value;
else
self.headers[current] = value;
};
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;
var headers = info.headers, key, value;
if (!headers) return;
for (var i = 0, n = headers.length; i < n; i += 2)
self.headers[headers[i].toLowerCase()] = headers[i+1];
};
for (var i = 0, n = headers.length; i < n; i += 2) {
key = headers[i].toLowerCase();
value = headers[i+1];
if (self.headers.hasOwnProperty(key))
self.headers[key] += ', ' + value;
else
self.headers[key] = value;
}
this._parser.onMessageComplete = this._parser[HTTPParser.kOnMessageComplete] = function() {
self._complete = true;
};
};
@@ -78,4 +89,3 @@ HttpParser.prototype.parse = function(data) {
};
module.exports = HttpParser;
+2 -1
View File
@@ -1,3 +1,5 @@
'use strict';
/**
Streams in a WebSocket connection
@@ -141,4 +143,3 @@ Messages.prototype.destroy = function() {};
exports.IO = IO;
exports.Messages = Messages;
+4 -4
View File
@@ -5,10 +5,11 @@
, "keywords" : ["websocket"]
, "license" : "MIT"
, "version" : "0.3.3"
, "engines" : {"node": ">=0.4.0"}
, "version" : "0.5.0"
, "engines" : {"node": ">=0.6.0"}
, "main" : "./lib/websocket/driver"
, "devDependencies" : {"jstest": ""}
, "dependencies" : {"websocket-extensions": ">=0.1.0"}
, "devDependencies" : {"jstest": "", "permessage-deflate": ""}
, "scripts" : {"test": "jstest spec/runner.js"}
@@ -18,4 +19,3 @@
, "bugs" : "http://github.com/faye/websocket-driver-node/issues"
}
-1
View File
@@ -42,4 +42,3 @@ require('./websocket/driver/draft75_spec')
require('./websocket/driver/draft76_spec')
require('./websocket/driver/hybi_spec')
require('./websocket/driver/client_spec')
+72 -1
View File
@@ -50,6 +50,14 @@ 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(
@@ -127,6 +135,70 @@ test.describe("Client", function() { with(this) {
}})
}})
describe("using a proxy", function() { with(this) {
it("sends a CONNECT request", function() { with(this) {
var proxy = driver().proxy("http://proxy.example.com")
expect(proxy, "emit").given("data", buffer(
"CONNECT www.example.com:80 HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"Connection: keep-alive\r\n" +
"Proxy-Connection: keep-alive\r\n" +
"\r\n"))
proxy.start()
}})
it("sends an authenticated CONNECT request", function() { with(this) {
var proxy = driver().proxy("http://user:pass@proxy.example.com")
expect(proxy, "emit").given("data", buffer(
"CONNECT www.example.com:80 HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"Connection: keep-alive\r\n" +
"Proxy-Connection: keep-alive\r\n" +
"Proxy-Authorization: Basic dXNlcjpwYXNz\r\n" +
"\r\n"))
proxy.start()
}})
it("sends a CONNECT request with custom headers", function() { with(this) {
var proxy = driver().proxy("http://user:pass@proxy.example.com")
proxy.setHeader("User-Agent", "Chrome")
expect(proxy, "emit").given("data", buffer(
"CONNECT www.example.com:80 HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"Connection: keep-alive\r\n" +
"Proxy-Connection: keep-alive\r\n" +
"Proxy-Authorization: Basic dXNlcjpwYXNz\r\n" +
"User-Agent: Chrome\r\n" +
"\r\n"))
proxy.start()
}})
describe("receiving a response", function() { with(this) {
before(function() { with(this) {
this.proxy = driver().proxy("http://proxy.example.com")
}})
it("returns true when the response is written", function() { with(this) {
// this prevents downstream connections suddenly closing for no reason
assertEqual( true, proxy.write(new Buffer("HTTP/1.1 200 OK\r\n\r\n")) )
}})
it("emits a 'connect' event when the proxy connects", function() { with(this) {
expect(proxy, "emit").given("connect")
expect(proxy, "emit").given("close")
expect(proxy, "emit").given("end")
proxy.write(new Buffer("HTTP/1.1 200 OK\r\n\r\n"))
}})
it("emits an 'error' event if the proxy does not connect", function() { with(this) {
expect(proxy, "emit").given("error", objectIncluding({message: "Can't establish a connection to the server at ws://www.example.com/socket"}))
expect(proxy, "emit").given("close")
expect(proxy, "emit").given("end")
proxy.write(new Buffer("HTTP/1.1 403 Forbidden\r\n\r\n"))
}})
}})
}})
describe("in the connecting state", function() { with(this) {
before(function() { this.driver().start() })
@@ -249,4 +321,3 @@ test.describe("Client", function() { with(this) {
}})
}})
}})
@@ -112,4 +112,3 @@ test.describe("draft-75", function() { with(this) {
}})
}})
}})
-1
View File
@@ -97,4 +97,3 @@ test.describe("Draft75", function() { with(this) {
itShouldBehaveLike("draft-75 protocol")
}})
-1
View File
@@ -184,4 +184,3 @@ test.describe("Draft76", function() { with(this) {
}})
}})
}})
-1
View File
@@ -539,4 +539,3 @@ test.describe("Hybi", function() { with(this) {
}})
}})
}})