Compare commits

..

39 Commits

Author SHA1 Message Date
James Coglan b861f1a779 Woops, left a console.log in. 2013-05-12 15:04:10 +01:00
James Coglan 047f87e4cd Set status and headers on error, not just on open. 2013-05-12 14:44:57 +01:00
James Coglan d6728e115d Add support for custom handshake headers. 2013-05-12 14:39:30 +01:00
James Coglan 15aedb7ca3 Make mention of streams more prominent. 2013-05-05 09:54:47 +01:00
James Coglan 7e9149faff Remove .redcar from .npmignore. 2013-05-05 01:51:52 +01:00
James Coglan 7fc369fc82 Bump version to 0.5.0. 2013-05-05 01:47:37 +01:00
James Coglan ec4ba0eb74 Use pipe() for the Autobahn echo client. 2013-05-05 00:01:17 +01:00
James Coglan d9475f5b07 Don't end the EventSource when piping a file into it. 2013-05-04 21:37:46 +01:00
James Coglan c68b3fdc46 Remove the websocket-driver submodule. 2013-05-04 20:45:26 +01:00
James Coglan 827af89809 Refer to 3rd 'upgrade' argument as 'body' instead of 'head'. 2013-05-04 17:37:53 +01:00
James Coglan 0d503a31e3 Return a normal HTTP response if the request is not a WebSocket in the documentation. 2013-05-04 17:33:57 +01:00
James Coglan 99fa7a57ca Various bits of stream-related refactoring. 2013-05-04 17:30:07 +01:00
James Coglan b0b39b8325 websocket-protocol is now websocket-driver. 2013-05-04 16:43:03 +01:00
James Coglan 77ee71c678 A few updates to the readme. 2013-05-04 15:09:45 +01:00
James Coglan fc759a66e9 s/this/self/ because otherwise error dispatching doesn't work. 2013-05-02 01:26:16 +01:00
James Coglan 459499d6d6 Expose parser error events to the user. 2013-05-02 00:12:56 +01:00
James Coglan 30d3eda575 Don't return anything from EventTarget methods. 2013-05-01 09:07:55 +01:00
James Coglan 699c023286 Turn EventTarget into a proxy to EventEmitter since classes are now Streams. 2013-05-01 08:57:53 +01:00
James Coglan 9ff5d28799 Implement EventSource as a writable stream. 2013-05-01 08:50:27 +01:00
James Coglan 43bd4dceb3 Change port used for tests. 2013-05-01 01:14:51 +01:00
James Coglan 971968be85 Rename supportedProtos to protocols. 2013-05-01 01:03:27 +01:00
James Coglan 2a2b7dfb11 Bump websocket-protocol. 2013-05-01 01:00:14 +01:00
James Coglan 83925f68d3 The protocol library now uses a version property instead of a getVersion() method. 2013-05-01 00:56:23 +01:00
James Coglan ee3212fada Implement the read/write stream interface on the API class. 2013-05-01 00:32:14 +01:00
James Coglan 789759bf07 Move most WebSocket logic into the API class so that the server and client classes just deal with getting a parser and setting up an IO stream. 2013-05-01 00:14:05 +01:00
James Coglan 3baa12600a Write the request body to the IO stream before piping the request socket in. 2013-04-30 23:48:11 +01:00
James Coglan 339cc7ad1c Bump websocket-protocol module and support legacy stream interface. 2013-04-30 23:36:54 +01:00
James Coglan fdd5cc6db2 Migrate to new protocol event API. 2013-04-30 09:07:06 +01:00
James Coglan 36d4e053d8 Bump the websocket-protocol submodule. 2013-04-29 22:35:28 +01:00
James Coglan 347e5df9a2 Bump the websocket-protocol submodule. 2013-04-29 21:51:58 +01:00
James Coglan 96d31cf694 Remove the hixie protocol handlers, they're now in websocket-protocol. 2013-04-29 21:45:03 +01:00
James Coglan 4ccd6f5a59 Bump the websocket-protocol submodule. 2013-04-29 21:36:46 +01:00
James Coglan 56f9b8ec4d Write the head to the handler so that draft-76 works. 2013-04-29 20:47:03 +01:00
James Coglan 217b32c383 Bump the websocket-protocol submodule. 2013-04-29 20:28:05 +01:00
James Coglan 6571e070a0 Remove some unneeded test code. 2013-04-29 19:08:38 +01:00
James Coglan 7e5d85278d Fix TCP error-catching. We should probably fix this in websocket-protocol instead, maybe. 2013-04-29 00:59:42 +01:00
James Coglan 178b2eebde Reinstate the exported Client. 2013-04-29 00:46:16 +01:00
James Coglan e6dd7b0749 Remove old Hybi parsers and update websocket-protocol submodule. 2013-04-29 00:20:01 +01:00
James Coglan ad4ca36a8c Move protocol handlers into a submodule. 2013-04-28 19:08:10 +01:00
37 changed files with 1126 additions and 2845 deletions
-3
View File
@@ -6,6 +6,3 @@ node_js:
- "0.10"
- "0.11"
before_install:
- '[ "${TRAVIS_NODE_VERSION}" = "0.6" ] && npm conf set strict-ssl false || true'
+55 -24
View File
@@ -1,42 +1,73 @@
### 0.3.4 / 2014-05-08
### 0.6.0 / 2013-05-12
* Don't hold memory-leaking references to I/O buffers after they have been parsed
* Add support for custom headers
### 0.3.3 / 2014-04-24
* Correct the draft-76 status line reason phrase
### 0.5.0 / 2013-05-05
### 0.3.2 / 2013-12-29
* Extract the protocol handlers into the `websocket-driver` library
* Support the Node streaming API
* 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
### 0.4.4 / 2013-02-14
* 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`
* Emit the `close` event if TCP is closed before CLOSE frame is acked
### 0.3.0 / 2013-09-09
* Support client URLs with Basic Auth credentials
### 0.4.3 / 2012-07-09
### 0.2.2 / 2013-07-05
* Add `Connection: close` to EventSource response
* Handle situations where `request.socket` is undefined
* No functional changes, just updates to package.json
### 0.2.1 / 2013-05-17
### 0.4.2 / 2012-04-06
* Export the isSecureRequest() method since faye-websocket relies on it
* Queue sent messages in the client's initial state
* Add WebSocket error code `1011`.
* Handle URLs with no path correctly by sending `GET /`
### 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.4.1 / 2012-02-26
### 0.1.0 / 2013-05-04
* Treat anything other than a `Buffer` as a string when calling `send()`
* First stable release
### 0.4.0 / 2012-02-13
* Add `ping()` method to server-side `WebSocket` and `EventSource`
* Buffer `send()` calls until the draft-76 handshake is complete
* Fix HTTPS problems on Node 0.7
### 0.3.1 / 2012-01-16
* Call `setNoDelay(true)` on `net.Socket` objects to reduce latency
### 0.3.0 / 2012-01-13
* Add support for `EventSource` connections
### 0.2.0 / 2011-12-21
* Add support for `Sec-WebSocket-Protocol` negotiation
* Support `hixie-76` close frames and 75/76 ignored segments
* Improve performance of HyBi parsing/framing functions
* Decouple parsers from TCP and reduce write volume
### 0.1.2 / 2011-12-05
* Detect closed sockets on the server side when TCP connection breaks
* Make `hixie-76` sockets work through HAProxy
### 0.1.1 / 2011-11-30
* Fix `addEventListener()` interface methods
### 0.1.0 / 2011-11-27
* Initial release, based on WebSocket components from Faye
+191 -211
View File
@@ -1,291 +1,271 @@
# websocket-driver [![Build Status](https://travis-ci.org/faye/websocket-driver-node.png)](https://travis-ci.org/faye/websocket-driver-node)
# faye-websocket
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
the protocol details from the I/O layer, such that users only need to implement
code to stream data in and out of it without needing to know anything about how
the protocol actually works. Think of it as a complete WebSocket system with
pluggable I/O.
* Travis CI build: [![Build
status](https://secure.travis-ci.org/faye/faye-websocket-node.png)](http://travis-ci.org/faye/faye-websocket-node)
* Autobahn tests: [server](http://faye.jcoglan.com/autobahn/servers/),
[client](http://faye.jcoglan.com/autobahn/clients/)
Due to this design, you get a lot of things for free. In particular, if you
hook this module up to some I/O object, it will do all of this for you:
This is a general-purpose WebSocket implementation extracted from the
[Faye](http://faye.jcoglan.com) project. It provides classes for easily
building WebSocket servers and clients in Node. It does not provide a server
itself, but rather makes it easy to handle WebSocket connections within an
existing [Node](http://nodejs.org/) application. It does not provide any
abstraction other than the standard [WebSocket
API](http://dev.w3.org/html5/websockets/).
* Select the correct server-side driver to talk to the client
* 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`
* 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
* Recombine fragmented messages
* Dispatch text, binary, ping and close frames
* Manage the socket-closing handshake process
* Automatically reply to ping frames with a matching pong
* Apply masking to messages sent by the client
This library was originally extracted from the [Faye](http://faye.jcoglan.com)
project but now aims to provide simple WebSocket support for any Node-based
project.
It also provides an abstraction for handling
[EventSource](http://dev.w3.org/html5/eventsource/) connections, which are
one-way connections that allow the server to push data to the client. They are
based on streaming HTTP responses and can be easier to access via proxies than
WebSockets.
## Installation
```
$ npm install websocket-driver
$ npm install faye-websocket
```
## Usage
## Handling WebSocket connections in Node
This module provides protocol drivers that have the same interface on the
server and on the client. A WebSocket driver is an object with two duplex
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 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
WebSocket, and if so you can create a driver and attach the request's I/O
stream to it.
You can handle WebSockets on the server side by listening for HTTP Upgrade
requests, and creating a new socket for the request. This socket object exposes
the usual WebSocket methods for receiving and sending messages. For example this
is how you'd implement an echo server:
```js
var http = require('http'),
websocket = require('websocket-driver');
var WebSocket = require('faye-websocket'),
http = require('http');
var server = http.createServer();
server.on('upgrade', function(request, socket, body) {
if (!websocket.isWebSocket(request)) return;
var driver = websocket.http(request);
driver.io.write(body);
socket.pipe(driver.io).pipe(socket);
driver.messages.on('data', function(message) {
console.log('Got a message', message);
});
driver.start();
if (WebSocket.isWebSocket(request)) {
var ws = new WebSocket(request, socket, body);
ws.on('message', function(event) {
ws.send(event.data);
});
ws.on('close', function(event) {
console.log('close', event.code, event.reason);
ws = null;
});
}
});
server.listen(8000);
```
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:
`WebSocket` objects are also duplex streams, so you could replace the
`ws.on('message', ...)` line with:
```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);
ws.pipe(ws);
```
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.
Note that under certain circumstances (notably a draft-76 client connecting
through an HTTP proxy), the WebSocket handshake will not be complete after you
call `new WebSocket()` because the server will not have received the entire
handshake from the client yet. In this case, calls to `ws.send()` will buffer
the message in memory until the handshake is complete, at which point any
buffered messages will be sent to the client.
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
passing in a URL. After this you use the driver API as described below to
process incoming data and send outgoing data.
If you need to detect when the WebSocket handshake is complete, you can use the
`onopen` event.
If the connection's protocol version supports it, you can call `ws.ping()` to
send a ping message and wait for the client's response. This method takes a
message string, and an optional callback that fires when a matching pong
message is received. It returns `true` iff a ping message was sent. If the
client does not support ping/pong, this method sends no data and returns
`false`.
```js
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.pipe(driver.io).pipe(tcp);
driver.messages.on('data', function(message) {
console.log('Got a message', message);
});
tcp.on('connect', function() {
driver.start();
ws.ping('Mic check, one, two', function() {
// fires when pong is received
});
```
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
## Using the WebSocket client
### Driver API
Drivers are created using one of the following methods:
The client supports both the plain-text `ws` protocol and the encrypted `wss`
protocol, and has exactly the same interface as a socket you would use in a web
browser. On the wire it identifies itself as `hybi-13`.
```js
driver = websocket.http(request, options)
driver = websocket.server(options)
driver = websocket.client(url, options)
var WebSocket = require('faye-websocket'),
ws = new WebSocket.Client('ws://www.example.com/');
ws.on('open', function(event) {
console.log('open');
ws.send('Hello, world!');
});
ws.on('message', function(event) {
console.log('message', event.data);
});
ws.on('close', function(event) {
console.log('close', event.code, event.reason);
ws = null;
});
```
The `http` method returns a driver chosen using the headers from a Node HTTP
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 WebSocket client also lets you inspect the status and headers of the
handshake response via its `statusCode` and `headers` properties.
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.
## Subprotocol negotiation
A driver has two duplex streams attached to it:
The WebSocket protocol allows peers to select and identify the application
protocol to use over the connection. On the client side, you can set which
protocols the client accepts by passing a list of protocol names when you
construct the socket:
* <b>`driver.io`</b> - this stream should be attached to an I/O socket like a
TCP stream. Pipe incoming TCP chunks to this stream for them to be parsed,
and pipe this stream back into TCP to send outgoing frames.
* <b>`driver.messages`</b> - this stream emits messages received over the
WebSocket. Writing to it sends messages to the other peer by emitting frames
via the `driver.io` stream.
```js
var ws = new WebSocket.Client('ws://www.example.com/', ['irc', 'amqp']);
```
All drivers respond to the following API methods, but some of them are no-ops
depending on whether the client supports the behaviour.
On the server side, you can likewise pass in the list of protocols the server
supports after the other constructor arguments:
Note that most of these methods are commands: if they produce data that should
be sent over the socket, they will give this to you by emitting `data` events
on the `driver.io` stream.
```js
var ws = new WebSocket(request, socket, body, ['irc', 'amqp']);
```
#### `driver.on('open', function(event) {})`
If the client and server agree on a protocol, both the client- and server-side
socket objects expose the selected protocol through the `ws.protocol` property.
Sets the callback to execute when the socket becomes open.
#### `driver.on('message', function(event) {})`
## Initialization options
Sets the callback to execute when a message is received. `event` will have a
`data` attribute containing either a string in the case of a text message or a
`Buffer` in the case of a binary message.
Both the server- and client-side classes allow an options object to be passed
in at initialization time, for example:
You can also listen for messages using the `driver.messages.on('data')` event,
which emits strings for text messages and buffers for binary messages.
```js
var ws = new WebSocket(request, socket, body, protocols, options);
var ws = new WebSocket.Client(url, protocols, options);
```
#### `driver.on('error', function(event) {})`
`protocols` is an array of subprotocols as described above, or `null`.
`options` is an optional object containing any of these fields:
Sets the callback to execute when a protocol error occurs due to the other peer
sending an invalid byte sequence. `event` will have a `message` attribute
describing the error.
* `headers` - an object containing key-value pairs representing HTTP headers to
be sent during the handshake process
* `ping` - an integer that sets how often the WebSocket should send ping
frames, measured in seconds
#### `driver.on('close', function(event) {})`
Sets the callback to execute when the socket becomes closed. The `event` object
has `code` and `reason` attributes.
## WebSocket API
#### `driver.setHeader(name, value)`
Both server- and client-side `WebSocket` objects support the following API.
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.
* <b>`on('open', function(event) {})`</b> fires when the socket connection is
established. Event has no attributes.
* <b>`on('message', function(event) {})`</b> fires when the socket receives a
message. Event has one attribute, <b>`data`</b>, which is either a `String`
(for text frames) or a `Buffer` (for binary frames).
* <b>`on('error', function(event) {})`</b> fires when there is a protocol error
due to bad data sent by the other peer. This event is purely informational,
you do not need to implement error recover.
* <b>`on('close', function(event) {})`</b> fires when either the client or the
server closes the connection. Event has two optional attributes,
<b>`code`</b> and <b>`reason`</b>, that expose the status code and message
sent by the peer that closed the connection.
* <b>`send(message)`</b> accepts either a `String` or a `Buffer` and sends a
text or binary message over the connection to the other peer.
* <b>`ping(message = '', function() {})`</b> sends a ping frame with an
optional message and fires the callback when a matching pong is received.
* <b>`close(code, reason)`</b> closes the connection, sending the given status
code and reason text, both of which are optional.
* <b>`version`</b> is a string containing the version of the `WebSocket`
protocol the connection is using.
* <b>`protocol`</b> is a string (which may be empty) identifying the
subprotocol the socket is using.
#### `driver.start()`
Initiates the protocol by sending the handshake - either the response for a
server-side driver or the request for a client-side one. This should be the
first method you invoke. Returns `true` iff a handshake was sent.
## Handling EventSource connections in Node
#### `driver.parse(string)`
EventSource connections provide a very similar interface, although because they
only allow the server to send data to the client, there is no `onmessage` API.
EventSource allows the server to push text messages to the client, where each
message has an optional event-type and ID.
Takes a string and parses it, potentially resulting in message events being
emitted (see `on('message')` above) or in data being sent to `driver.io`. You
should send all data you receive via I/O to this method by piping a stream into
`driver.io`.
```js
var WebSocket = require('faye-websocket'),
EventSource = WebSocket.EventSource,
http = require('http');
#### `driver.text(string)`
var server = http.createServer();
Sends a text message over the socket. If the socket handshake is not yet
complete, the message will be queued until it is. Returns `true` if the message
was sent or queued, and `false` if the socket can no longer send messages.
server.on('request', function(request, response) {
if (EventSource.isEventSource(request)) {
var es = new EventSource(request, response);
console.log('open', es.url, es.lastEventId);
// Periodically send messages
var loop = setInterval(function() { es.send('Hello') }, 1000);
es.on('close', function() {
clearInterval(loop);
es = null;
});
} else {
// Normal HTTP request
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end('Hello');
}
});
This method is equivalent to `driver.messages.write(string)`.
server.listen(8000);
```
#### `driver.binary(buffer)`
The `send` method takes two optional parameters, `event` and `id`. The default
event-type is `'message'` with no ID. For example, to send a `notification`
event with ID `99`:
Takes a `Buffer` and sends it as a binary message. Will queue and return `true`
or `false` the same way as the `text` method. It will also return `false` if
the driver does not support binary messages.
```js
es.send('Breaking News!', {event: 'notification', id: '99'});
```
This method is equivalent to `driver.messages.write(buffer)`.
The `EventSource` object exposes the following properties:
#### `driver.ping(string = '', function() {})`
* <b>`url`</b> is a string containing the URL the client used to create the
EventSource.
* <b>`lastEventId`</b> is a string containing the last event ID received by the
client. You can use this when the client reconnects after a dropped
connection to determine which messages need resending.
Sends a ping frame over the socket, queueing it if necessary. `string` and the
callback are both optional. If a callback is given, it will be invoked when the
socket receives a pong frame whose content matches `string`. Returns `false` if
frames can no longer be sent, or if the driver does not support ping/pong.
When you initialize an EventSource with ` new EventSource()`, you can pass
configuration options after the `response` parameter. Available options are:
#### `driver.close()`
* <b>`retry`</b> is a number that tells the client how long (in seconds) it
should wait after a dropped connection before attempting to reconnect.
* <b>`ping`</b> is a number that tells the server how often (in seconds) to
send 'ping' packets to the client to keep the connection open, to defeat
timeouts set by proxies. The client will ignore these messages.
Initiates the closing handshake if the socket is still open. For drivers with
no closing handshake, this will result in the immediate execution of the
`on('close')` driver. For drivers with a closing handshake, this sends a
closing frame and `emit('close')` will execute when a response is received or a
protocol error occurs.
For example, this creates a connection that pings every 15 seconds and is
retryable every 10 seconds if the connection is broken:
#### `driver.version`
```js
var es = new EventSource(request, response, {ping: 15, retry: 10});
```
Returns the WebSocket version in use as a string. Will either be `hixie-75`,
`hixie-76` or `hybi-$version`.
#### `driver.protocol`
Returns a string containing the selected subprotocol, if any was agreed upon
using the `Sec-WebSocket-Protocol` mechanism. This value becomes available
after `emit('open')` has fired.
You can send a ping message at any time by calling `es.ping()`. Unlike
WebSocket, the client does not send a response to this; it is merely to send
some data over the wire to keep the connection alive.
## License
(The MIT License)
Copyright (c) 2010-2014 James Coglan
Copyright (c) 2010-2013 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
+38
View File
@@ -0,0 +1,38 @@
var WebSocket = require('../lib/faye/websocket'),
pace = require('pace');
var host = 'ws://localhost:9001',
agent = 'Node ' + process.version,
cases = 0,
skip = [];
var socket = new WebSocket.Client(host + '/getCaseCount'),
progress;
socket.onmessage = function(event) {
console.log('Total cases to run: ' + event.data);
cases = parseInt(event.data);
progress = pace(cases);
};
socket.onclose = function() {
var runCase = function(n) {
progress.op();
if (n > cases) {
socket = new WebSocket.Client(host + '/updateReports?agent=' + encodeURIComponent(agent));
socket.onclose = process.exit;
} else if (skip.indexOf(n) >= 0) {
runCase(n + 1);
} else {
socket = new WebSocket.Client(host + '/runCase?case=' + n + '&agent=' + encodeURIComponent(agent));
socket.pipe(socket);
socket.on('close', function() { runCase(n + 1) });
}
};
runCase(1);
};
+24
View File
@@ -0,0 +1,24 @@
var WebSocket = require('../lib/faye/websocket'),
port = process.argv[2] || 7000,
secure = process.argv[3] === 'ssl',
scheme = secure ? 'wss' : 'ws',
url = scheme + '://localhost:' + port + '/',
headers = {Origin: 'http://faye.jcoglan.com'},
ws = new WebSocket.Client(url, null, {headers: headers});
console.log('Connecting to ' + ws.url);
ws.onopen = function(event) {
console.log('open');
ws.send('Hello, WebSocket!');
};
ws.onmessage = function(event) {
console.log('message', event.data);
// ws.close(1002, 'Going away');
};
ws.onclose = function(event) {
console.log('close', event.code, event.reason);
};
+21
View File
@@ -0,0 +1,21 @@
defaults
mode http
timeout client 5s
timeout connect 5s
timeout server 5s
frontend all 0.0.0.0:3000
mode http
timeout client 120s
option forwardfor
option http-server-close
option http-pretend-keepalive
default_backend sockets
backend sockets
balance uri depth 2
timeout server 120s
server socket1 127.0.0.1:7000
+68
View File
@@ -0,0 +1,68 @@
var WebSocket = require('../lib/faye/websocket'),
fs = require('fs'),
http = require('http'),
https = require('https');
var port = process.argv[2] || 7000,
secure = process.argv[3] === 'ssl';
var upgradeHandler = function(request, socket, head) {
var ws = new WebSocket(request, socket, head, ['irc', 'xmpp'], {ping: 5});
console.log('open', ws.url, ws.version, ws.protocol);
ws.pipe(ws);
ws.onclose = function(event) {
console.log('close', event.code, event.reason);
ws = null;
};
};
var requestHandler = function(request, response) {
if (!WebSocket.EventSource.isEventSource(request))
return staticHandler(request, response);
var es = new WebSocket.EventSource(request, response),
time = parseInt(es.lastEventId, 10) || 0;
console.log('open', es.url, es.lastEventId);
var loop = setInterval(function() {
time += 1;
es.send('Time: ' + time);
setTimeout(function() {
if (es) es.send('Update!!', {event: 'update', id: time});
}, 1000);
}, 2000);
fs.createReadStream(__dirname + '/haproxy.conf').pipe(es, {end: false});
es.onclose = function() {
clearInterval(loop);
console.log('close', es.url);
es = null;
};
};
var staticHandler = function(request, response) {
var path = request.url;
fs.readFile(__dirname + path, function(err, content) {
var status = err ? 404 : 200;
response.writeHead(status, {'Content-Type': 'text/html'});
response.write(content || 'Not found');
response.end();
});
};
var server = secure
? https.createServer({
key: fs.readFileSync(__dirname + '/../spec/server.key'),
cert: fs.readFileSync(__dirname + '/../spec/server.crt')
})
: http.createServer();
server.on('request', requestHandler);
server.on('upgrade', upgradeHandler);
server.listen(port);
+39
View File
@@ -0,0 +1,39 @@
<!doctype html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>EventSource test</title>
</head>
<body>
<h1>EventSource test</h1>
<ul></ul>
<script type="text/javascript">
var logger = document.getElementsByTagName('ul')[0],
socket = new EventSource('/');
var log = function(text) {
logger.innerHTML += '<li>' + text + '</li>';
};
socket.onopen = function() {
log('OPEN');
};
socket.onmessage = function(event) {
log('MESSAGE: ' + event.data);
};
socket.addEventListener('update', function(event) {
log('UPDATE(' + event.lastEventId + '): ' + event.data);
});
socket.onerror = function(event) {
log('ERROR: ' + event.message);
};
</script>
</body>
</html>
-21
View File
@@ -1,21 +0,0 @@
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]);
+44
View File
@@ -0,0 +1,44 @@
<!doctype html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>WebSocket test</title>
</head>
<body>
<h1>WebSocket test</h1>
<ul></ul>
<script type="text/javascript">
var logger = document.getElementsByTagName('ul')[0],
Socket = window.MozWebSocket || window.WebSocket,
protos = ['foo', 'bar', 'xmpp'],
socket = new Socket('ws://' + location.hostname + ':' + location.port + '/', protos),
index = 0;
var log = function(text) {
logger.innerHTML += '<li>' + text + '</li>';
};
socket.addEventListener('open', function() {
log('OPEN: ' + socket.protocol);
socket.send('Hello, world');
});
socket.onerror = function(event) {
log('ERROR: ' + event.message);
};
socket.onmessage = function(event) {
log('MESSAGE: ' + event.data);
setTimeout(function() { socket.send(++index + ' ' + event.data) }, 2000);
};
socket.onclose = function(event) {
log('CLOSE: ' + event.code + ', ' + event.reason);
};
</script>
</body>
</html>
+125
View File
@@ -0,0 +1,125 @@
var Stream = require('stream').Stream,
util = require('util'),
driver = require('websocket-driver'),
API = require('./websocket/api'),
EventTarget = require('./websocket/api/event_target'),
Event = require('./websocket/api/event');
var EventSource = function(request, response, options) {
this.writable = true;
options = options || {};
this._stream = response.socket;
this._ping = options.ping || this.DEFAULT_PING;
this._retry = options.retry || this.DEFAULT_RETRY;
var scheme = driver.isSecureRequest(request) ? 'https:' : 'http:';
this.url = scheme + '//' + request.headers.host + request.url;
this.lastEventId = request.headers['last-event-id'] || '';
this.readyState = API.CONNECTING;
var self = this;
if (!this._stream || !this._stream.writable) return;
process.nextTick(function() { self._open() });
this._stream.setTimeout(0);
this._stream.setNoDelay(true);
var handshake = 'HTTP/1.1 200 OK\r\n' +
'Content-Type: text/event-stream\r\n' +
'Cache-Control: no-cache, no-store\r\n' +
'Connection: close\r\n' +
'\r\n\r\n' +
'retry: ' + Math.floor(this._retry * 1000) + '\r\n\r\n';
this._write(handshake);
this._stream.on('drain', function() { self.emit('drain') });
if (this._ping)
this._pingTimer = setInterval(function() { self.ping() }, this._ping * 1000);
['error', 'end'].forEach(function(event) {
self._stream.on(event, function() { self.close() });
});
};
util.inherits(EventSource, Stream);
EventSource.isEventSource = function(request) {
if (request.method !== 'GET') return false;
var accept = (request.headers.accept || '').split(/\s*,\s*/);
return accept.indexOf('text/event-stream') >= 0;
};
var instance = {
DEFAULT_PING: 10,
DEFAULT_RETRY: 5,
_write: function(chunk) {
if (!this.writable) return false;
try {
return this._stream.write(chunk, 'utf8');
} catch (e) {
return false;
}
},
_open: function() {
if (this.readyState !== API.CONNECTING) return;
this.readyState = API.OPEN;
var event = new Event('open');
event.initEvent('open', false, false);
this.dispatchEvent(event);
},
write: function(message) {
return this.send(message);
},
end: function(message) {
if (message !== undefined) this.write(message);
this.close();
},
send: function(message, options) {
if (this.readyState > API.OPEN) return false;
message = String(message).replace(/(\r\n|\r|\n)/g, '$1data: ');
options = options || {};
var frame = '';
if (options.event) frame += 'event: ' + options.event + '\r\n';
if (options.id) frame += 'id: ' + options.id + '\r\n';
frame += 'data: ' + message + '\r\n\r\n';
return this._write(frame);
},
ping: function() {
return this._write(':\r\n\r\n');
},
close: function() {
if (this.readyState > API.OPEN) return false;
this.readyState = API.CLOSED;
this.writable = false;
if (this._pingTimer) clearInterval(this._pingTimer);
if (this._stream) this._stream.end();
var event = new Event('close');
event.initEvent('close', false, false);
this.dispatchEvent(event);
return true;
}
};
for (var method in instance) EventSource.prototype[method] = instance[method];
for (var key in EventTarget) EventSource.prototype[key] = EventTarget[key];
module.exports = EventSource;
+44
View File
@@ -0,0 +1,44 @@
// API references:
//
// * http://dev.w3.org/html5/websockets/
// * http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#interface-eventtarget
// * http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#interface-event
var util = require('util'),
driver = require('websocket-driver'),
API = require('./websocket/api');
var WebSocket = function(request, socket, body, protocols, options) {
this._stream = socket;
this._driver = driver.http(request, {protocols: protocols});
var self = this;
if (!this._stream || !this._stream.writable) return;
var catchup = function() { self._stream.removeListener('data', catchup) };
this._stream.on('data', catchup);
this._stream.setTimeout(0);
this._stream.setNoDelay(true);
this._driver.io.write(body);
API.call(this, options);
['error', 'end'].forEach(function(event) {
this._stream.on(event, function() { self._finalize('', 1006) });
}, this);
this._driver.start();
};
util.inherits(WebSocket, API);
WebSocket.isWebSocket = function(request) {
return driver.isWebSocket(request);
};
WebSocket.WebSocket = WebSocket;
WebSocket.Client = require('./websocket/client');
WebSocket.EventSource = require('./eventsource');
module.exports = WebSocket;
+132
View File
@@ -0,0 +1,132 @@
var Stream = require('stream').Stream,
util = require('util'),
EventTarget = require('./api/event_target'),
Event = require('./api/event');
var API = function(options) {
options = options || {};
this.readable = this.writable = true;
var headers = options.headers;
if (headers) {
for (var name in headers) this._driver.setHeader(name, headers[name]);
}
this._ping = options.ping;
this._pingId = 0;
this.readyState = API.CONNECTING;
this.bufferedAmount = 0;
this.protocol = '';
this.url = this._driver.url;
this.version = this._driver.version;
var self = this;
this._driver.on('open', function(e) { self._open() });
this._driver.on('message', function(e) { self._receiveMessage(e.data) });
this._driver.on('close', function(e) { self._finalize(e.reason, e.code) });
this._driver.on('error', function(error) {
var event = new Event('error', {message: error.message});
event.initEvent('error', false, false);
self.dispatchEvent(event);
});
this.on('error', function() {});
this._driver.messages.on('drain', function() {
self.emit('drain');
});
if (this._ping)
this._pingTimer = setInterval(function() {
self._pingId += 1;
self.ping(self._pingId.toString());
}, this._ping * 1000);
this._stream.pipe(this._driver.io);
this._driver.io.pipe(this._stream);
};
util.inherits(API, Stream);
API.CONNECTING = 0;
API.OPEN = 1;
API.CLOSING = 2;
API.CLOSED = 3;
var instance = {
write: function(data) {
return this.send(data);
},
end: function(data) {
if (data !== undefined) this.send(data);
this.close();
},
pause: function() {
return this._driver.messages.pause();
},
resume: function() {
return this._driver.messages.resume();
},
send: function(data) {
if (this.readyState > API.OPEN) return false;
if (!(data instanceof Buffer)) data = String(data);
return this._driver.messages.write(data);
},
ping: function(message, callback) {
if (this.readyState > API.OPEN) return false;
return this._driver.ping(message, callback);
},
close: function() {
if (this.readyState === API.OPEN) this.readyState = API.CLOSING;
this._driver.close();
},
_open: function() {
if (this.readyState !== API.CONNECTING) return;
this.readyState = API.OPEN;
this.protocol = this._driver.protocol || '';
var event = new Event('open');
event.initEvent('open', false, false);
this.dispatchEvent(event);
},
_receiveMessage: function(data) {
if (this.readyState > API.OPEN) return false;
if (this.readable) this.emit('data', data);
var event = new Event('message', {data: data});
event.initEvent('message', false, false);
this.dispatchEvent(event);
},
_finalize: function(reason, code) {
if (this.readyState === API.CLOSED) return;
if (this._pingTimer) clearInterval(this._pingTimer);
if (this._stream) this._stream.end();
if (this.readable) this.emit('end');
this.readable = this.writable = false;
this.readyState = API.CLOSED;
var event = new Event('close', {code: code || 1000, reason: reason || ''});
event.initEvent('close', false, false);
this.dispatchEvent(event);
}
};
for (var method in instance) API.prototype[method] = instance[method];
for (var key in EventTarget) API.prototype[key] = EventTarget[key];
module.exports = API;
+21
View File
@@ -0,0 +1,21 @@
var Event = function(eventType, options) {
this.type = eventType;
for (var key in options)
this[key] = options[key];
};
Event.prototype.initEvent = function(eventType, canBubble, cancelable) {
this.type = eventType;
this.bubbles = canBubble;
this.cancelable = cancelable;
};
Event.prototype.stopPropagation = function() {};
Event.prototype.preventDefault = function() {};
Event.CAPTURING_PHASE = 1;
Event.AT_TARGET = 2;
Event.BUBBLING_PHASE = 3;
module.exports = Event;
+29
View File
@@ -0,0 +1,29 @@
var Event = require('./event');
var EventTarget = {
onopen: null,
onmessage: null,
onerror: null,
onclose: null,
addEventListener: function(eventType, listener, useCapture) {
this.on(eventType, listener);
},
removeEventListener: function(eventType, listener, useCapture) {
this.removeListener(eventType, listener);
},
dispatchEvent: function(event) {
event.target = event.currentTarget = this;
event.eventPhase = Event.AT_TARGET;
if (this['on' + event.type])
this['on' + event.type](event);
this.emit(event.type, event);
}
};
module.exports = EventTarget;
+45
View File
@@ -0,0 +1,45 @@
var util = require('util'),
net = require('net'),
tls = require('tls'),
driver = require('websocket-driver'),
API = require('./api');
var Client = function(url, protocols, options) {
this.url = url;
this._uri = require('url').parse(url);
this._driver = driver.client(url, {protocols: protocols});
['open', 'error'].forEach(function(event) {
this._driver.on(event, function() {
self.headers = self._driver.headers;
self.statusCode = self._driver.statusCode;
});
}, this);
var secure = (this._uri.protocol === 'wss:'),
onConnect = function() { self._driver.start() },
tlsOptions = {},
self = this;
if (options && options.verify === false) tlsOptions.rejectUnauthorized = false;
var connection = secure
? tls.connect(this._uri.port || 443, this._uri.hostname, tlsOptions, onConnect)
: net.createConnection(this._uri.port || 80, this._uri.hostname);
this._stream = connection;
this._stream.setTimeout(0);
this._stream.setNoDelay(true);
if (!secure) this._stream.on('connect', onConnect);
API.call(this, options);
['error', 'end'].forEach(function(event) {
this._stream.on(event, function() { self._finalize('', 1006) });
}, this);
};
util.inherits(Client, API);
module.exports = Client;
-44
View File
@@ -1,44 +0,0 @@
// Protocol references:
//
// * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
// * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
// * http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17
var Client = require('./driver/client'),
Server = require('./driver/server');
var Driver = {
client: function(url, options) {
options = options || {};
if (options.masking === undefined) options.masking = true;
return new Client(url, options);
},
server: function(options) {
options = options || {};
if (options.requireMasking === undefined) options.requireMasking = true;
return new Server(options);
},
http: function() {
return Server.http.apply(Server, arguments);
},
isSecureRequest: function(request) {
return Server.isSecureRequest(request);
},
isWebSocket: function(request) {
if (request.method !== 'GET') return false;
var connection = request.headers.connection || '',
upgrade = request.headers.upgrade || '';
return request.method === 'GET' &&
connection.toLowerCase().split(/\s*,\s*/).indexOf('upgrade') >= 0 &&
upgrade.toLowerCase() === 'websocket';
}
};
module.exports = Driver;
-126
View File
@@ -1,126 +0,0 @@
var Emitter = require('events').EventEmitter,
util = require('util'),
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;
this.io = new streams.IO(this);
this.messages = new streams.Messages(this);
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());
if (this._stage !== -1) this._open();
return true;
},
text: function(message) {
return this.frame(message);
},
binary: function(message) {
return false;
},
ping: function() {
return false;
},
close: function(reason, code) {
if (this.readyState !== 1) return false;
this.readyState = 3;
this.emit('close', new Base.CloseEvent(null, null));
return true;
},
_open: function() {
this.readyState = 1;
this.__queue.forEach(function(args) { this.frame.apply(this, args) }, this);
this.__queue = [];
this.emit('open', new Base.OpenEvent());
},
_queue: function(message) {
this.__queue.push(message);
return true;
},
_write: function(chunk) {
var io = this.io;
if (io.readable) io.emit('data', chunk);
}
};
for (var key in instance)
Base.prototype[key] = instance[key];
Base.ConnectEvent = function() {};
Base.OpenEvent = function() {};
Base.CloseEvent = function(code, reason) {
this.code = code;
this.reason = reason;
};
Base.MessageEvent = function(data) {
this.data = data;
};
module.exports = Base;
-112
View File
@@ -1,112 +0,0 @@
var url = require('url'),
util = require('util'),
HttpParser = require('./http_parser'),
Base = require('./base'),
Hybi = require('./hybi');
var Client = function(url, options) {
this.version = 'hybi-13';
Hybi.call(this, null, url, options);
this.readyState = -1;
this._key = Client.generateKey();
this._accept = Hybi.generateAccept(this._key);
this._http = new HttpParser('response');
};
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');
};
var instance = {
start: function() {
if (this.readyState !== -1) return false;
this._write(this._handshakeRequest());
this.readyState = 0;
return true;
},
parse: function(data) {
if (this.readyState > 0) return Hybi.prototype.parse.call(this, data);
this._http.parse(data);
if (!this._http.isComplete()) return;
this._validateHandshake();
this.parse(this._http.body);
},
_handshakeRequest: function() {
var uri = url.parse(this.url);
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'
];
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');
},
_failHandshake: function(message) {
message = 'Error during WebSocket handshake: ' + message;
this.emit('error', new Error(message));
this.readyState = 3;
this.emit('close', new Base.CloseEvent(this.ERRORS.protocol_error, message));
},
_validateHandshake: function() {
this.statusCode = this._http.statusCode;
this.headers = this._http.headers;
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");
if (upgrade.toLowerCase() !== 'websocket')
return this._failHandshake("'Upgrade' header value is not 'WebSocket'");
if (connection === '')
return this._failHandshake("'Connection' header is missing");
if (connection.toLowerCase() !== 'upgrade')
return this._failHandshake("'Connection' header value is not 'Upgrade'");
if (accept !== this._accept)
return this._failHandshake('Sec-WebSocket-Accept mismatch');
this.protocol = null;
if (protocol !== '') {
if (this._protocols.indexOf(protocol) < 0)
return this._failHandshake('Sec-WebSocket-Protocol mismatch');
else
this.protocol = protocol;
}
this._open();
}
};
for (var key in instance)
Client.prototype[key] = instance[key];
module.exports = Client;
-118
View File
@@ -1,118 +0,0 @@
var Base = require('./base'),
util = require('util');
var Draft75 = function(request, url, options) {
Base.apply(this, arguments);
this._stage = 0;
this.version = 'hixie-75';
};
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];
switch (this._stage) {
case -1:
this._body.push(data);
this._sendHandshakeBody();
break;
case 0:
this._parseLeadingByte(data);
break;
case 1:
value = (data & 0x7F);
this._length = value + 128 * this._length;
if (this._closing && this._length === 0) {
return this.close();
}
else if ((0x80 & data) !== 0x80) {
if (this._length === 0) {
this._stage = 0;
}
else {
this._skipped = 0;
this._stage = 2;
}
}
break;
case 2:
if (data === 0xFF) {
message = new Buffer(this._buffer).toString('utf8', 0, this._buffer.length);
this.emit('message', new Base.MessageEvent(message));
this._stage = 0;
}
else {
if (this._length) {
this._skipped += 1;
if (this._skipped === this._length)
this._stage = 0;
} else {
this._buffer.push(data);
if (this._buffer.length > this._maxLength) return this.close();
}
}
break;
}
}
},
frame: function(data) {
if (this.readyState === 0) return this._queue([data]);
if (this.readyState > 1) return false;
var buffer = new Buffer(data, 'utf8'),
frame = new Buffer(buffer.length + 2);
frame[0] = 0x00;
frame[buffer.length + 1] = 0xFF;
buffer.copy(frame, 1);
this._write(frame);
return true;
},
_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');
},
_parseLeadingByte: function(data) {
if ((0x80 & data) === 0x80) {
this._length = 0;
this._stage = 1;
} else {
delete this._length;
delete this._skipped;
this._buffer = [];
this._stage = 2;
}
}
};
for (var key in instance)
Draft75.prototype[key] = instance[key];
module.exports = Draft75;
-109
View File
@@ -1,109 +0,0 @@
var Base = require('./base'),
Draft75 = require('./draft75'),
crypto = require('crypto'),
util = require('util');
var numberFromKey = function(key) {
return parseInt(key.match(/[0-9]/g).join(''), 10);
};
var spacesInKey = function(key) {
return key.match(/ /g).length;
};
var bigEndian = function(number) {
var string = '';
[24, 16, 8, 0].forEach(function(offset) {
string += String.fromCharCode(number >> offset & 0xFF);
});
return string;
};
var Draft76 = function(request, url, options) {
Draft75.apply(this, arguments);
this._stage = -1;
this._body = [];
this.version = 'hixie-76';
};
util.inherits(Draft76, Draft75);
var instance = {
BODY_SIZE: 8,
start: function() {
if (!Draft75.prototype.start.call(this)) return false;
this._started = true;
this._sendHandshakeBody();
return true;
},
close: function() {
if (this.readyState === 3) return false;
this._write(new Buffer([0xFF, 0x00]));
this.readyState = 3;
this.emit('close', new Base.CloseEvent(null, null));
return true;
},
_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');
},
_handshakeSignature: function() {
if (this._body.length < this.BODY_SIZE) return null;
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));
md5.update(bigEndian(value2));
md5.update(body.toString('binary'));
return new Buffer(md5.digest('binary'), 'binary');
},
_sendHandshakeBody: function() {
if (!this._started) return;
var signature = this._handshakeSignature();
if (!signature) return;
this._write(signature);
this._stage = 0;
this._open();
if (this._body.length > this.BODY_SIZE)
this.parse(this._body.slice(this.BODY_SIZE));
},
_parseLeadingByte: function(data) {
if (data !== 0xFF)
return Draft75.prototype._parseLeadingByte.call(this, data);
this._closing = true;
this._length = 0;
this._stage = 1;
}
};
for (var key in instance)
Draft76.prototype[key] = instance[key];
module.exports = Draft76;
-30
View File
@@ -1,30 +0,0 @@
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;
-81
View File
@@ -1,81 +0,0 @@
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;
-429
View File
@@ -1,429 +0,0 @@
var crypto = require('crypto'),
util = require('util'),
Base = require('./base'),
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._requireMasking = this._options.requireMasking;
this._pingCallbacks = {};
if (!this.version) {
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;
offset = offset || 0;
for (var i = 0, n = payload.length - offset; i < n; i++) {
payload[offset + i] = payload[offset + i] ^ mask[i % 4];
}
return payload;
};
Hybi.generateAccept = function(key) {
var sha1 = crypto.createHash('sha1');
sha1.update(key + Hybi.GUID);
return sha1.digest('base64');
};
Hybi.GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
var instance = {
BYTE: 255,
FIN: 128,
MASK: 128,
RSV1: 64,
RSV2: 32,
RSV3: 16,
OPCODE: 15,
LENGTH: 127,
OPCODES: {
continuation: 0,
text: 1,
binary: 2,
close: 8,
ping: 9,
pong: 10
},
OPCODE_CODES: [0, 1, 2, 8, 9, 10],
FRAGMENTED_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) }),
ERRORS: {
normal_closure: 1000,
going_away: 1001,
protocol_error: 1002,
unacceptable: 1003,
encoding_error: 1007,
policy_violation: 1008,
too_large: 1009,
extension_error: 1010,
unexpected_condition: 1011
},
ERROR_CODES: [1000, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011],
MIN_RESERVED_ERROR: 3000,
MAX_RESERVED_ERROR: 4999,
// 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})*$/,
parse: function(data) {
this._reader.put(data);
var buffer = true;
while (buffer) {
switch (this._stage) {
case 0:
buffer = this._reader.read(1);
if (buffer) this._parseOpcode(buffer[0]);
break;
case 1:
buffer = this._reader.read(1);
if (buffer) this._parseLength(buffer[0]);
break;
case 2:
buffer = this._reader.read(this._lengthSize);
if (buffer) this._parseExtendedLength(buffer);
break;
case 3:
buffer = this._reader.read(4);
if (buffer) {
this._mask = buffer;
this._stage = 4;
}
break;
case 4:
buffer = this._reader.read(this._length);
if (buffer) {
this._payload = buffer;
this._emitFrame();
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 !== 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');
},
binary: function(message) {
return this.frame(message, 'binary');
},
ping: function(message, callback) {
message = message || '';
if (callback) this._pingCallbacks[message] = callback;
return this.frame(message, 'ping');
},
close: function(reason, code) {
reason = reason || '';
code = code || this.ERRORS.normal_closure;
if (this.readyState === 0) {
this.readyState = 3;
this.emit('close', new Base.CloseEvent(code, reason));
return true;
} else if (this.readyState === 1) {
this.frame(reason, 'close', code);
this.readyState = 2;
return true;
} else {
return false;
}
},
_handshakeResponse: function() {
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,
headers = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
'Sec-WebSocket-Accept: ' + accept
];
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(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));
},
_fail: function(type, message) {
this.emit('error', new Error(message));
this._shutdown(this.ERRORS[type], message);
},
_parseOpcode: function(data) {
var rsvs = [this.RSV1, this.RSV2, this.RSV3].map(function(rsv) {
return (data & rsv) === rsv;
});
if (rsvs.filter(function(rsv) { return rsv }).length > 0)
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));
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);
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)
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)
return this._fail('unacceptable', 'Received unmasked frame but masking is required');
this._length = (data & this.LENGTH);
if (this._length >= 0 && this._length <= 125) {
if (!this._checkFrameLength()) return;
this._stage = this._masked ? 3 : 4;
} else {
this._lengthSize = (this._length === 126 ? 2 : 8);
this._stage = 2;
}
},
_parseExtendedLength: function(buffer) {
this._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._checkFrameLength()) return;
this._stage = this._masked ? 3 : 4;
},
_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() {
var payload = Hybi.mask(this._payload, this._mask),
opcode = this._opcode;
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));
}
}
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);
}
}
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 (!(payload.length === 0) &&
!(code !== null && code >= this.MIN_RESERVED_ERROR && code <= this.MAX_RESERVED_ERROR) &&
this.ERROR_CODES.indexOf(code) < 0)
code = this.ERRORS.protocol_error;
if (payload.length > 125 || (payload.length > 2 && !reason))
code = this.ERRORS.protocol_error;
this._shutdown(code, reason || '');
}
else 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];
delete callbacks[message];
if (callback) callback()
}
},
_buffer: function(fragment) {
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.__blength = 0;
},
_encode: function(buffer) {
try {
var string = buffer.toString('binary', 0, buffer.length);
if (!this.UTF8_MATCH.test(string)) return null;
} catch (e) {}
return buffer.toString('utf8', 0, buffer.length);
},
_getInteger: function(bytes) {
var number = 0;
for (var i = 0, n = bytes.length; i < n; i++)
number += bytes[i] * this.TWO_POWERS[n - 1 - i];
return number;
}
};
for (var key in instance)
Hybi.prototype[key] = instance[key];
module.exports = Hybi;
@@ -1,42 +0,0 @@
var StreamReader = function() {
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.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, size;
while (remain > 0 && i < n) {
chunk = queue[i];
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, this._cursor === 0 ? i : i - 1);
return buffer;
};
module.exports = StreamReader;
-104
View File
@@ -1,104 +0,0 @@
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.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;
-144
View File
@@ -1,144 +0,0 @@
/**
Streams in a WebSocket connection
---------------------------------
We model a WebSocket as two duplex streams: one stream is for the wire protocol
over an I/O socket, and the other is for incoming/outgoing messages.
+----------+ +---------+ +----------+
[1] write(chunk) -->| ~~~~~~~~ +----->| parse() +----->| ~~~~~~~~ +--> emit('data') [2]
| | +----+----+ | |
| | | | |
| IO | | [5] | Messages |
| | V | |
| | +---------+ | |
[4] emit('data') <--+ ~~~~~~~~ |<-----+ frame() |<-----+ ~~~~~~~~ |<-- write(chunk) [3]
+----------+ +---------+ +----------+
Message transfer in each direction is simple: IO receives a byte stream [1] and
sends this stream for parsing. The parser will periodically emit a complete
message text on the Messages stream [2]. Similarly, when messages are written
to the Messages stream [3], they are framed using the WebSocket wire format and
emitted via IO [4].
There is a feedback loop via [5] since some input from [1] will be things like
ping, pong and close frames. In these cases the protocol responds by emitting
responses directly back to [4] rather than emitting messages via [2].
For the purposes of flow control, we consider the sources of each Readable
stream to be as follows:
* [2] receives input from [1]
* [4] receives input from [1] and [3]
The classes below express the relationships described above without prescribing
anything about how parse() and frame() work, other than assuming they emit
'data' events to the IO and Messages streams. They will work with any protocol
driver having these two methods.
**/
var Stream = require('stream').Stream,
util = require('util');
var IO = function(driver) {
this.readable = this.writable = true;
this._paused = false;
this._driver = driver;
};
util.inherits(IO, Stream);
// The IO pause() and resume() methods will be called when the socket we are
// piping to gets backed up and drains. Since IO output [4] comes from IO input
// [1] and Messages input [3], we need to tell both of those to return false
// from write() when this stream is paused.
IO.prototype.pause = function() {
this._paused = true;
this._driver.messages._paused = true;
};
IO.prototype.resume = function() {
this._paused = false;
this.emit('drain');
var messages = this._driver.messages;
messages._paused = false;
messages.emit('drain');
};
// When we receive input from a socket, send it to the parser and tell the
// source whether to back off.
IO.prototype.write = function(chunk) {
if (!this.writable) return false;
this._driver.parse(chunk);
return !this._paused;
};
// The IO end() method will be called when the socket piping into it emits
// 'close' or 'end', i.e. the socket is closed. In this situation the Messages
// stream will not emit any more data so we emit 'end'.
IO.prototype.end = function(chunk) {
if (!this.writable) return;
if (chunk !== undefined) this.write(chunk);
this.writable = false;
var messages = this._driver.messages;
if (messages.readable) {
messages.readable = messages.writable = false;
messages.emit('end');
}
};
IO.prototype.destroy = function() {
this.end();
};
var Messages = function(driver) {
this.readable = this.writable = true;
this._paused = false;
this._driver = driver;
};
util.inherits(Messages, Stream);
// The Messages pause() and resume() methods will be called when the app that's
// processing the messages gets backed up and drains. If we're emitting
// messages too fast we should tell the source to slow down. Message output [2]
// comes from IO input [1].
Messages.prototype.pause = function() {
this._driver.io._paused = true;
};
Messages.prototype.resume = function() {
this._driver.io._paused = false;
this._driver.io.emit('drain');
};
// When we receive messages from the user, send them to the formatter and tell
// the source whether to back off.
Messages.prototype.write = function(message) {
if (!this.writable) return false;
this._driver.frame(message);
return !this._paused;
};
// The Messages end() method will be called when a stream piping into it emits
// 'end'. Many streams may be piped into the WebSocket and one of them ending
// does not mean the whole socket is done, so just process the input and move
// on leaving the socket open.
Messages.prototype.end = function(message) {
if (message !== undefined) this.write(message);
};
Messages.prototype.destroy = function() {};
exports.IO = IO;
exports.Messages = Messages;
+19 -13
View File
@@ -1,21 +1,27 @@
{ "name" : "websocket-driver"
, "description" : "WebSocket protocol handler with pluggable I/O"
, "homepage" : "http://github.com/faye/websocket-driver-node"
{ "name" : "faye-websocket"
, "description" : "Standards-compliant WebSocket server and client"
, "homepage" : "http://github.com/faye/faye-websocket-node"
, "author" : "James Coglan <jcoglan@gmail.com> (http://jcoglan.com/)"
, "keywords" : ["websocket"]
, "license" : "MIT"
, "keywords" : ["websocket", "eventsource"]
, "version" : "0.3.4"
, "version" : "0.6.0"
, "engines" : {"node": ">=0.4.0"}
, "main" : "./lib/websocket/driver"
, "devDependencies" : {"jstest": ""}
, "main" : "./lib/faye/websocket"
, "dependencies" : {"websocket-driver": ">=0.2.0"}
, "devDependencies" : {"jsclass": "", "pace": ""}
, "scripts" : {"test": "jstest spec/runner.js"}
, "scripts" : {"test": "node spec/runner.js"}
, "repository" : { "type" : "git"
, "url" : "git://github.com/faye/websocket-driver-node.git"
}
, "bugs" : "http://github.com/faye/faye-websocket-node/issues"
, "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/faye-websocket-node.git"
}
]
}
+171
View File
@@ -0,0 +1,171 @@
var Client = require('../../../lib/faye/websocket/client')
JS.ENV.WebSocketSteps = JS.Test.asyncSteps({
server: function(port, secure, callback) {
this._adapter = new EchoServer()
this._adapter.listen(port, secure)
this._port = port
setTimeout(callback, 100)
},
stop: function(callback) {
this._adapter.stop()
setTimeout(callback, 100)
},
open_socket: function(url, protocols, callback) {
var done = false,
self = this,
resume = function(open) {
if (done) return
done = true
self._open = open
callback()
}
this._ws = new Client(url, protocols, {verify: false})
this._ws.onopen = function() { resume(true) }
this._ws.onclose = function() { resume(false) }
},
close_socket: function(callback) {
var self = this
this._ws.onclose = function() {
self._open = false
callback()
}
this._ws.close()
},
check_open: function(callback) {
this.assert( this._open )
callback()
},
check_closed: function(callback) {
this.assert( !this._open )
callback()
},
check_protocol: function(protocol, callback) {
this.assertEqual( protocol, this._ws.protocol )
callback()
},
listen_for_message: function(callback) {
var self = this
this._ws.addEventListener('message', function(message) { self._message = message.data })
callback()
},
send_message: function(message, callback) {
this._ws.send(message)
setTimeout(callback, 100)
},
check_response: function(message, callback) {
this.assertEqual( message, this._message )
callback()
},
check_no_response: function(callback) {
this.assert( !this._message )
callback()
}
})
JS.ENV.ClientSpec = JS.Test.describe("Client", function() { with(this) {
include(WebSocketSteps)
before(function() {
this.protocols = ["foo", "echo"]
this.plain_text_url = "ws://localhost:4180/bayeux"
this.secure_url = "wss://localhost:4180/bayeux"
this.port = 4180
})
sharedBehavior("socket client", function() { with(this) {
it("can open a connection", function() { with(this) {
open_socket(socket_url, protocols)
check_open()
check_protocol("echo")
}})
it("can close the connection", function() { with(this) {
open_socket(socket_url, protocols)
close_socket()
check_closed()
}})
describe("in the OPEN state", function() { with(this) {
before(function() { with(this) {
open_socket(socket_url, protocols)
}})
it("can send and receive messages", function() { with(this) {
listen_for_message()
send_message("I expect this to be echoed")
check_response("I expect this to be echoed")
}})
it("sends numbers as strings", function() { with(this) {
listen_for_message()
send_message(13)
check_response("13")
}})
it("sends booleans as strings", function() { with(this) {
listen_for_message()
send_message(false)
check_response("false")
}})
it("sends arrays as strings", function() { with(this) {
listen_for_message()
send_message([13,14,15])
check_response("13,14,15")
}})
}})
describe("in the CLOSED state", function() { with(this) {
before(function() { with(this) {
open_socket(socket_url, protocols)
close_socket()
}})
it("cannot send and receive messages", function() { with(this) {
listen_for_message()
send_message("I expect this to be echoed")
check_no_response()
}})
}})
}})
describe("with a plain-text server", function() { with(this) {
before(function() {
this.socket_url = this.plain_text_url
this.blocked_url = this.secure_url
})
before(function() { this.server(4180, false) })
after (function() { this.stop() })
behavesLike("socket client")
}})
describe("with a secure server", function() { with(this) {
before(function() {
this.socket_url = this.secure_url
this.blocked_url = this.plain_text_url
})
before(function() { this.server(4180, true) })
after (function() { this.stop() })
behavesLike("socket client")
}})
}})
+30 -38
View File
@@ -1,45 +1,37 @@
var test = require('jstest').Test,
Stream = require('stream').Stream,
util = require('util')
require('jsclass')
var BufferMatcher = function(data) {
this._data = (typeof data === 'string')
? new Buffer(data, 'utf8')
: new Buffer(data)
var WebSocket = require('../lib/faye/websocket'),
fs = require('fs'),
http = require('http'),
https = require('https')
JS.ENV.EchoServer = function() {}
EchoServer.prototype.listen = function(port, ssl) {
var server = ssl
? https.createServer({
key: fs.readFileSync(__dirname + '/server.key'),
cert: fs.readFileSync(__dirname + '/server.crt')
})
: http.createServer()
server.on('upgrade', function(request, socket, head) {
var ws = new WebSocket(request, socket, head, ["echo"])
ws.pipe(ws)
})
this._httpServer = server
server.listen(port)
}
BufferMatcher.prototype.equals = function(other) {
if (this._data.length !== other.length) return false;
for (var i = 0, n = other.length; i < n; i++) {
if (other[i] !== this._data[i]) return false;
}
return true;
EchoServer.prototype.stop = function(callback, scope) {
this._httpServer.on('close', function() {
if (callback) callback.call(scope);
});
this._httpServer.close();
}
var Collector = function() {
this.bytes = []
this.writable = true
}
util.inherits(Collector, Stream)
Collector.prototype.write = function(buffer) {
this.bytes = []
for (var i = 0, n = buffer.length; i < n; i++) {
this.bytes[i] = buffer[i]
}
return true
}
test.Unit.TestCase.include({
buffer: function(data) {
return new BufferMatcher(data)
},
collector: function() {
return this._collector = this._collector || new Collector()
}
JS.require('JS.Test', function() {
require('./faye/websocket/client_spec')
JS.Test.autorun()
})
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')
+15
View File
@@ -0,0 +1,15 @@
-----BEGIN CERTIFICATE-----
MIICZTCCAc4CCQDxyrJZrFA0vjANBgkqhkiG9w0BAQUFADB3MQswCQYDVQQGEwJV
SzEPMA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xDTALBgNVBAoTBEZh
eWUxFTATBgNVBAMTDEphbWVzIENvZ2xhbjEgMB4GCSqGSIb3DQEJARYRamNvZ2xh
bkBnbWFpbC5jb20wHhcNMTEwODMwMTIzOTM2WhcNMTIwODI5MTIzOTM2WjB3MQsw
CQYDVQQGEwJVSzEPMA0GA1UECBMGTG9uZG9uMQ8wDQYDVQQHEwZMb25kb24xDTAL
BgNVBAoTBEZheWUxFTATBgNVBAMTDEphbWVzIENvZ2xhbjEgMB4GCSqGSIb3DQEJ
ARYRamNvZ2xhbkBnbWFpbC5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGB
AMDjU5fAK7fvUCZIYHcGXDZD/m9bY+B/UcwGcowk0hMQGYNlLKrpiK7xXBmZpDb6
r8n+7L/epBeSumbRIm4TDzeNHhuQGYLIeGQy7JNLoPBr6GxubjuJhKOOBnCqcupR
CLGG7Zw5oL4UvtZVH6kL9XnjyokQQbxxeoV9DqtqOaHHAgMBAAEwDQYJKoZIhvcN
AQEFBQADgYEAvQjSpzE1ahaeH1CmbLwckTxvWMZfxcZOrxTruK1po3cNnDOjGqFQ
KEkNj3K5WfwTBD4QgUdYDykhDX2m6HaMz4JEbgrwQv8M8FiswIA3dyGsbOifOk8H
r3GPNKMzm4o6vrn6RGOpt9q6bsWUBUHfNpP93uU2C9QEwDua3cFjDA0=
-----END CERTIFICATE-----
+15
View File
@@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDA41OXwCu371AmSGB3Blw2Q/5vW2Pgf1HMBnKMJNITEBmDZSyq
6Yiu8VwZmaQ2+q/J/uy/3qQXkrpm0SJuEw83jR4bkBmCyHhkMuyTS6Dwa+hsbm47
iYSjjgZwqnLqUQixhu2cOaC+FL7WVR+pC/V548qJEEG8cXqFfQ6rajmhxwIDAQAB
AoGABlk1DiCQD8y7mZb2PdSiwlJ4lFewsNnf6lQn/v7TPzdfb5ir4LAxBHkDLACH
jBuyH3bZefMs+W2l3u5xMKhF7uJqYcUlJdH2UwRfNG54Hn4SGAjQOK3ONer99sUf
USlsWSX1HjAAFMCBwUfKxMZA3VNQfYKTPdm0jSVf85kHO1ECQQD3s6ksm3QpfD0L
eG9EoDrqmwnEfpKoWPpz1O0i5tY9VcmhmLwS5Zpd7lB1qjTqzZk4RygU73T/BseJ
azehIHK5AkEAx1mSXt+ec8RfzVi/io6oqi2vOcACXRbOG4NQmqUWPnumdwsJjsjR
RzEoDFC2lu6448p9sgEq+CkbmgVeiyp4fwJAQnmgySve/NMuvslPcyddKGD7OhSN
30ghzrwx98/jZwqC1i9bKeccimDOjwVitjD/Ea9m/ldVGqwDGMoBX+iJYQJAEIOO
CYfyw1pQKV2huGOq+zX/nwQV7go2lrbhFX55gkGR/6iNaSOfmosq6yJAje5GqLAc
i4NnQNl+7NpnA5ZIFwJBAI1+OsZyjbRI99pYkTdOpa5IPlIb3j3JbSfjAWHLxlRY
0HLvN3Q1mE9kbB+uKH6syF/S7nALgsLgq7eHYvIaE/A=
-----END RSA PRIVATE KEY-----
-252
View File
@@ -1,252 +0,0 @@
var Client = require("../../../lib/websocket/driver/client"),
test = require('jstest').Test
test.describe("Client", function() { with(this) {
define("options", function() {
return this._options = this._options || {protocols: this.protocols()}
})
define("protocols", function() {
null
})
define("url", function() {
return "ws://www.example.com/socket"
})
define("driver", function() {
if (this._driver) return this._driver
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 })
this._driver.on('error', function(e) { self.error = e })
this._driver.on('close', function(e) { self.close = [e.code, e.reason] })
var collector = this.collector()
this._driver.io.on("data", function(d) { collector.write(d) })
return this._driver
})
define("key", function() {
return "2vBVWg4Qyk3ZoM/5d3QD9Q=="
})
define("response", function() {
return "HTTP/1.1 101 Switching Protocols\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Accept: QV3I5XUXU2CdhtjixE7QCkCcMZM=\r\n" +
"\r\n"
})
before(function() {
this.stub(Client, "generateKey").returns(this.key())
this.open = this.error = this.close = false
this.message = ""
})
describe("in the beginning state", function() { with(this) {
it("starts in no state", function() { with(this) {
assertEqual( null, driver().getState() )
}})
describe("start", function() { with(this) {
it("writes the handshake request to the socket", 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" +
"\r\n"))
driver().start()
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().start() )
}})
describe("with subprotocols", function() { with(this) {
define("protocols", function() { return ["foo", "bar", "xmpp"] })
it("writes the handshake with Sec-WebSocket-Protocol", 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" +
"Sec-WebSocket-Protocol: foo, bar, xmpp\r\n" +
"\r\n"))
driver().start()
}})
}})
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() )
}})
}})
}})
describe("in the connecting state", function() { with(this) {
before(function() { this.driver().start() })
describe("with a valid response", function() { with(this) {
before(function() { this.driver().parse(new Buffer(this.response())) })
it("changes the state to open", function() { with(this) {
assertEqual( true, open )
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) {
before(function() { with(this) {
var resp = new Buffer(response().length + 4)
new Buffer(response()).copy(resp)
new Buffer([0x81, 0x02, 72, 105]).copy(resp, resp.length - 4)
driver().parse(resp)
}})
it("changes the state to open", function() { with(this) {
assertEqual( true, open )
assertEqual( false, close )
assertEqual( "open", driver().getState() )
}})
it("parses the frame", function() { with(this) {
assertEqual( "Hi", message )
}})
}})
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")
this.driver().parse(new Buffer(resp))
})
it("changes the state to closed", function() { with(this) {
assertEqual( false, open )
assertEqual( "Error during WebSocket handshake: 'Upgrade' header value is not 'WebSocket'", error.message )
assertEqual( [1002, "Error during WebSocket handshake: 'Upgrade' header value is not 'WebSocket'"], close )
assertEqual( "closed", driver().getState() )
}})
}})
describe("with a bad Accept header", function() { with(this) {
before(function() {
var resp = this.response().replace(/QV3/g, "wrong")
this.driver().parse(new Buffer(resp))
})
it("changes the state to closed", function() { with(this) {
assertEqual( false, open )
assertEqual( "Error during WebSocket handshake: Sec-WebSocket-Accept mismatch", error.message )
assertEqual( [1002, "Error during WebSocket handshake: Sec-WebSocket-Accept mismatch"], close )
assertEqual( "closed", driver().getState() )
}})
}})
describe("with valid subprotocols", function() { with(this) {
define("protocols", function() { return ["foo", "xmpp"] })
before(function() {
var resp = this.response().replace(/\r\n\r\n/, "\r\nSec-WebSocket-Protocol: xmpp\r\n\r\n")
this.driver().parse(new Buffer(resp))
})
it("changs the state to open", function() { with(this) {
assertEqual( true, open )
assertEqual( false, close )
assertEqual( "open", driver().getState() )
}})
it("selects the subprotocol", function() { with(this) {
assertEqual( "xmpp", driver().protocol )
}})
}})
describe("with invalid subprotocols", function() { with(this) {
define("protocols", function() { return ["foo", "xmpp"] })
before(function() {
var resp = this.response().replace(/\r\n\r\n/, "\r\nSec-WebSocket-Protocol: irc\r\n\r\n")
this.driver().parse(new Buffer(resp))
})
it("changs the state to closed", function() { with(this) {
assertEqual( false, open )
assertEqual( "Error during WebSocket handshake: Sec-WebSocket-Protocol mismatch", error.message )
assertEqual( [1002, "Error during WebSocket handshake: Sec-WebSocket-Protocol mismatch"], close )
assertEqual( "closed", driver().getState() )
}})
it("selects no subprotocol", function() { with(this) {
assertEqual( null, driver().protocol )
}})
}})
}})
}})
-115
View File
@@ -1,115 +0,0 @@
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() })
describe("parse", function() { with(this) {
it("parses text frames", function() { with(this) {
driver().parse([0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff])
assertEqual( "Hello", message )
}})
it("parses multiple frames from the same packet", function() { with(this) {
driver().parse([0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff])
assertEqual( "HelloHello", message )
}})
it("parses text frames beginning 0x00-0x7F", function() { with(this) {
driver().parse([0x66, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff])
assertEqual( "Hello", message )
}})
it("ignores frames with a length header", function() { with(this) {
driver().parse([0x80, 0x02, 0x48, 0x65, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff])
assertEqual( "Hello", message )
}})
it("parses multibyte text frames", function() { with(this) {
driver().parse([0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff])
assertEqual( "Apple = ", message )
}})
it("parses frames received in several packets", function() { with(this) {
driver().parse([0x00, 0x41, 0x70, 0x70, 0x6c, 0x65])
driver().parse([0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff])
assertEqual( "Apple = ", message )
}})
it("parses fragmented frames", function() { with(this) {
driver().parse([0x00, 0x48, 0x65, 0x6c])
driver().parse([0x6c, 0x6f, 0xff])
assertEqual( "Hello", message )
}})
}})
describe("frame", function() { with(this) {
it("formats the given string as a WebSocket frame", function() { with(this) {
driver().frame("Hello")
assertEqual( [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff], collector().bytes )
}})
it("encodes multibyte characters correctly", function() { with(this) {
driver().frame("Apple = ")
assertEqual( [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff], collector().bytes )
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().frame("lol") )
}})
}})
describe("ping", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().ping()
}})
it("returns false", function() { with(this) {
assertEqual( false, driver().ping() )
}})
}})
describe("close", function() { with(this) {
it("triggers the onclose event", function() { with(this) {
driver().close()
assertEqual( true, close )
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().close() )
}})
it("changes the state to closed", function() { with(this) {
driver().close()
assertEqual( "closed", driver().getState() )
}})
}})
}})
describe("in the closed state", function() { with(this) {
before(function() {
this.driver().start()
this.driver().close()
})
describe("close", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().close()
}})
it("returns false", function() { with(this) {
assertEqual( false, driver().close() )
}})
it("leaves the protocol in the closed state", function() { with(this) {
driver().close()
assertEqual( "closed", driver().getState() )
}})
}})
}})
}})
}})
-100
View File
@@ -1,100 +0,0 @@
var Draft75 = require("../../../lib/websocket/driver/draft75"),
test = require('jstest').Test
test.describe("Draft75", function() { with(this) {
define("request", function() {
return this._request = this._request || {
headers: {
"connection": "Upgrade",
"upgrade": "WebSocket",
"origin": "http://www.example.com"
}
}
})
define("options", function() {
return this._options = this._options || {masking: false}
})
define("driver", function() {
if (this._driver) return this._driver
this._driver = new Draft75(this.request(), "ws://www.example.com/socket", this.options())
var self = this
this._driver.on('open', function(e) { self.open = true })
this._driver.on('message', function(e) { self.message += e.data })
this._driver.on('close', function(e) { self.close = true })
this._driver.io.pipe(this.collector())
return this._driver
})
before(function() {
this.open = this.close = false
this.message = ""
})
describe("in the connecting state", function() { with(this) {
it("starts in the connecting state", function() { with(this) {
assertEqual( "connecting", driver().getState() )
}})
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" +
"Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n" +
"WebSocket-Origin: http://www.example.com\r\n" +
"WebSocket-Location: ws://www.example.com/socket\r\n" +
"\r\n"))
driver().start()
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().start() )
}})
it("triggers the onopen event", function() { with(this) {
driver().start()
assertEqual( true, open )
}})
it("changes the state to open", function() { with(this) {
driver().start()
assertEqual( "open", driver().getState() )
}})
it("sets the protocol version", function() { with(this) {
driver().start()
assertEqual( "hixie-75", driver().version )
}})
}})
describe("frame", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().frame("Hello, world")
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().frame("whatever") )
}})
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" +
"Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n" +
"WebSocket-Origin: http://www.example.com\r\n" +
"WebSocket-Location: ws://www.example.com/socket\r\n" +
"\r\n"))
expect(driver().io, "emit").given("data", buffer([0x00, 0x48, 0x69, 0xff]))
driver().frame("Hi")
driver().start()
}})
}})
}})
itShouldBehaveLike("draft-75 protocol")
}})
-187
View File
@@ -1,187 +0,0 @@
var Draft76 = require("../../../lib/websocket/driver/draft76"),
test = require('jstest').Test
test.describe("Draft76", function() { with(this) {
BODY = new Buffer([0x91, 0x25, 0x3e, 0xd3, 0xa9, 0xe7, 0x6a, 0x88])
define("body", function() {
return BODY
})
define("response", function() {
return [0xb4, 0x9c, 0x6e, 0x40, 0x53, 0x04, 0x04, 0x26, 0xe5, 0x1b, 0xbf, 0x6c, 0xb7, 0x9f, 0x1d, 0xf9]
})
define("request", function() {
return this._request = this._request || {
headers: {
"connection": "Upgrade",
"upgrade": "WebSocket",
"origin": "http://www.example.com",
"sec-websocket-key1": "1 38 wZ3f9 23O0 3l 0r",
"sec-websocket-key2": "27 0E 6 2 1665:< ;U 1H"
}
}
})
define("options", function() {
return this._options = this._options || {masking: false}
})
define("driver", function() {
if (this._driver) return this._driver
this._driver = new Draft76(this.request(), "ws://www.example.com/socket", this.options())
var self = this
this._driver.on('open', function(e) { self.open = true })
this._driver.on('message', function(e) { self.message += e.data })
this._driver.on('close', function(e) { self.close = true })
this._driver.io.pipe(this.collector())
this._driver.io.write(this.body())
return this._driver
})
before(function() {
this.open = this.close = false
this.message = ""
})
describe("in the connecting state", function() { with(this) {
it("starts in the connecting state", function() { with(this) {
assertEqual( "connecting", driver().getState() )
}})
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 WebSocket Protocol Handshake\r\n" +
"Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Origin: http://www.example.com\r\n" +
"Sec-WebSocket-Location: ws://www.example.com/socket\r\n" +
"\r\n"))
expect(driver().io, "emit").given("data", buffer(response()))
driver().start()
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().start() )
}})
it("triggers the onopen event", function() { with(this) {
driver().start()
assertEqual( true, open )
}})
it("changes the state to open", function() { with(this) {
driver().start()
assertEqual( "open", driver().getState() )
}})
it("sets the protocol version", function() { with(this) {
driver().start()
assertEqual( "hixie-76", driver().version )
}})
}})
describe("frame", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().frame("Hello, world")
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().frame("whatever") )
}})
it("queues the frames until the handshake has been sent", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"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" +
"Sec-WebSocket-Location: ws://www.example.com/socket\r\n" +
"\r\n"))
expect(driver().io, "emit").given("data", buffer(response()))
expect(driver().io, "emit").given("data", buffer([0x00, 72, 105, 0xff]))
driver().frame("Hi")
driver().start()
}})
}})
describe("with no request body", function() { with(this) {
define("body", function() {
return new Buffer([])
})
it("writes the handshake response with no body", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"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" +
"Sec-WebSocket-Location: ws://www.example.com/socket\r\n" +
"\r\n"))
driver().start()
}})
it("does not trigger the onopen event", function() { with(this) {
driver().start()
assertEqual( false, open )
}})
it("leaves the protocol in the connecting state", function() { with(this) {
driver().start()
assertEqual( "connecting", driver().getState() )
}})
describe("when the request body is received", function() { with(this) {
before(function() { this.driver().start() })
it("sends the response body", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(response()))
driver().parse(BODY)
}})
it("triggers the onopen event", function() { with(this) {
driver().parse(BODY)
assertEqual( true, open )
}})
it("changes the state to open", function() { with(this) {
driver().parse(BODY)
assertEqual( "open", driver().getState() )
}})
it("sends any frames queued before the handshake was complete", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(response()))
expect(driver().io, "emit").given("data", buffer([0x00, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0xff]))
driver().frame("hello")
driver().parse(BODY)
}})
}})
}})
}})
itShouldBehaveLike("draft-75 protocol")
describe("in the open state", function() { with(this) {
before(function() { this.driver().start() })
describe("parse", function() { with(this) {
it("closes the socket if a close frame is received", function() { with(this) {
driver().parse([0xff, 0x00])
assertEqual( true, close )
assertEqual( "closed", driver().getState() )
}})
}})
describe("close", function() { with(this) {
it("writes a close message to the socket", function() { with(this) {
expect(driver().io, "emit").given("data", buffer([0xff, 0x00]))
driver().close()
}})
}})
}})
}})
-542
View File
@@ -1,542 +0,0 @@
var Hybi = require("../../../lib/websocket/driver/hybi"),
test = require('jstest').Test
test.describe("Hybi", function() { with(this) {
define("request", function() {
return this._request = this._request || {
headers: {
"connection": "Upgrade",
"upgrade": "websocket",
"origin": "http://www.example.com",
// "sec-websocket-extensions": "x-webkit-deflate-frame",
"sec-websocket-key": "JFBCWHksyIpXV+6Wlq/9pw==",
"sec-websocket-version": "13"
}
}
})
define("options", function() {
return this._options = this._options || {masking: false}
})
define("driver", function() {
if (this._driver) return this._driver
this._driver = new Hybi(this.request(), "ws://www.example.com/socket", this.options())
var self = this
this._driver.on('open', function(e) { self.open = true })
this._driver.on('message', function(e) { self.message += e.data })
this._driver.on('error', function(e) { self.error = e })
this._driver.on('close', function(e) { self.close = [e.code, e.reason] })
this._driver.io.pipe(this.collector())
return this._driver
})
before(function() {
this.open = this.error = this.close = false
this.message = ""
})
describe("in the connecting state", function() { with(this) {
it("starts in the connecting state", function() { with(this) {
assertEqual( "connecting", driver().getState() )
}})
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 Switching Protocols\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" +
"\r\n"))
driver().start()
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().start() )
}})
describe("with subprotocols", function() { with(this) {
before(function() { with(this) {
request().headers["sec-websocket-protocol"] = "foo, bar, xmpp"
options().protocols = ["xmpp"]
}})
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" +
"Sec-WebSocket-Protocol: xmpp\r\n" +
"\r\n"))
driver().start()
}})
it("sets the subprotocol", function() { with(this) {
driver().start()
assertEqual( "xmpp", driver().protocol )
}})
}})
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 )
}})
it("changes the state to open", function() { with(this) {
driver().start()
assertEqual( "open", driver().getState() )
}})
it("sets the protocol version", function() { with(this) {
driver().start()
assertEqual( "hybi-13", driver().version )
}})
}})
describe("frame", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().frame("Hello, world")
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().frame("whatever") )
}})
it("queues the frames until the handshake has been send", 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" +
"\r\n"))
expect(driver().io, "emit").given("data", buffer([0x81, 0x02, 72, 105]))
driver().frame("Hi")
driver().start()
}})
}})
describe("ping", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().ping()
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().ping() )
}})
it("queues the ping until the handshake has been send", 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" +
"\r\n"))
expect(driver().io, "emit").given("data", buffer([0x89, 0]))
driver().ping()
driver().start()
}})
}})
describe("close", function() { with(this) {
it("does not write anything to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().close()
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().close() )
}})
it("triggers the onclose event", function() { with(this) {
driver().close()
assertEqual( [1000, ""], close )
}})
it("changes the state to closed", function() { with(this) {
driver().close()
assertEqual( "closed", driver().getState() )
}})
}})
}})
describe("in the open state", function() { with(this) {
before(function() { this.driver().start() })
describe("parse", function() { with(this) {
define("mask", function() {
return this._mask = this._mask ||
[1,2,3,4].map(function() { return Math.floor(Math.random() * 256) })
})
define("maskMessage", function(bytes) {
var output = []
for (var i = 0, n = bytes.length; i < n; i++) {
output[i] = bytes[i] ^ this.mask()[i % 4]
}
return output
})
it("parses unmasked text frames", function() { with(this) {
driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
assertEqual( "Hello", message )
}})
it("parses multiple frames from the same packet", function() { with(this) {
driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
assertEqual( "HelloHello", message )
}})
it("parses empty text frames", function() { with(this) {
driver().parse([0x81, 0x00])
assertEqual( "", message )
}})
it("parses fragmented text frames", function() { with(this) {
driver().parse([0x01, 0x03, 0x48, 0x65, 0x6c])
driver().parse([0x80, 0x02, 0x6c, 0x6f])
assertEqual( "Hello", message )
}})
it("parses masked text frames", function() { with(this) {
driver().parse([0x81, 0x85])
driver().parse(mask())
driver().parse(maskMessage([0x48, 0x65, 0x6c, 0x6c, 0x6f]))
assertEqual( "Hello", message )
}})
it("parses masked empty text frames", function() { with(this) {
driver().parse([0x81, 0x80])
driver().parse(mask())
driver().parse(maskMessage([]))
assertEqual( "", message )
}})
it("parses masked fragmented text frames", function() { with(this) {
driver().parse([0x01, 0x81])
driver().parse(mask())
driver().parse(maskMessage([0x48]))
driver().parse([0x80, 0x84])
driver().parse(mask())
driver().parse(maskMessage([0x65, 0x6c, 0x6c, 0x6f]))
assertEqual( "Hello", message )
}})
it("closes the socket if the frame has an unrecognized opcode", function() { with(this) {
driver().parse([0x83, 0x00])
assertEqual( [0x88, 0x1e, 0x03, 0xea], collector().bytes.slice(0,4) )
assertEqual( "Unrecognized frame opcode: 3", error.message )
assertEqual( [1002, "Unrecognized frame opcode: 3"], close )
assertEqual( "closed", driver().getState() )
}})
it("closes the socket if a close frame is received", function() { with(this) {
driver().parse([0x88, 0x07, 0x03, 0xe8, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
assertEqual( [0x88, 0x07, 0x03, 0xe8, 0x48, 0x65, 0x6c, 0x6c, 0x6f], collector().bytes )
assertEqual( [1000, "Hello"], close )
assertEqual( "closed", driver().getState() )
}})
it("parses unmasked multibyte text frames", function() { with(this) {
driver().parse([0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf])
assertEqual( "Apple = ", message )
}})
it("parses frames received in several packets", function() { with(this) {
driver().parse([0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c])
driver().parse([0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf])
assertEqual( "Apple = ", message )
}})
it("parses fragmented multibyte text frames", function() { with(this) {
driver().parse([0x01, 0x0a, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3])
driver().parse([0x80, 0x01, 0xbf])
assertEqual( "Apple = ", message )
}})
it("parse masked multibyte text frames", function() { with(this) {
driver().parse([0x81, 0x8b])
driver().parse(mask())
driver().parse(maskMessage([0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf]))
assertEqual( "Apple = ", message )
}})
it("parses masked fragmented multibyte text frames", function() { with(this) {
driver().parse([0x01, 0x8a])
driver().parse(mask())
driver().parse(maskMessage([0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3]))
driver().parse([0x80, 0x81])
driver().parse(mask())
driver().parse(maskMessage([0xbf]))
assertEqual( "Apple = ", message )
}})
it("parses unmasked medium-length text frames", function() { with(this) {
driver().parse([0x81, 0x7e, 0x00, 0xc8])
var i = 40, result = ""
while (i--) {
driver().parse([0x48, 0x65, 0x6c, 0x6c, 0x6f])
result += "Hello"
}
assertEqual( result, message )
}})
it("returns an error for too-large frames", function() { with(this) {
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() )
}})
it("parses masked medium-length text frames", function() { with(this) {
driver().parse([0x81, 0xfe, 0x00, 0xc8])
driver().parse(mask())
var i = 40, result = "", packet = []
while (i--) {
packet = packet.concat([0x48, 0x65, 0x6c, 0x6c, 0x6f])
result += "Hello"
}
driver().parse(maskMessage(packet))
assertEqual( result, message )
}})
it("replies to pings with a pong", function() { with(this) {
driver().parse([0x89, 0x04, 0x4f, 0x48, 0x41, 0x49])
assertEqual( [0x8a, 0x04, 0x4f, 0x48, 0x41, 0x49], collector().bytes )
}})
}})
describe("frame", function() { with(this) {
it("formats the given string as a WebSocket frame", function() { with(this) {
driver().frame("Hello")
assertEqual( [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f], collector().bytes )
}})
it("formats a byte array as a binary WebSocket frame", function() { with(this) {
driver().frame([0x48, 0x65, 0x6c])
assertEqual( [0x82, 0x03, 0x48, 0x65, 0x6c], collector().bytes )
}})
it("encodes multibyte characters correctly", function() { with(this) {
driver().frame("Apple = ")
assertEqual( [0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf], collector().bytes )
}})
it("encodes medium-length strings using extra length bytes", function() { with(this) {
var i = 40, frame = [0x81, 0x7e, 0x00, 0xc8], string = ""
while (i--) {
string += "Hello"
frame = frame.concat([0x48, 0x65, 0x6c, 0x6c, 0x6f])
}
driver().frame(string)
assertEqual( frame, collector(). bytes )
}})
it("encodes close frames with an error code", function() { with(this) {
driver().frame("Hello", "close", 1002)
assertEqual( [0x88, 0x07, 0x03, 0xea, 0x48, 0x65, 0x6c, 0x6c, 0x6f], collector().bytes )
}})
it("encodes pong frames", function() { with(this) {
driver().frame("", "pong")
assertEqual( [0x8a, 0x00], collector().bytes )
}})
}})
describe("ping", function() { with(this) {
it("writes a ping frame to the socket", function() { with(this) {
driver().ping("mic check")
assertEqual( [0x89, 0x09, 0x6d, 0x69, 0x63, 0x20, 0x63, 0x68, 0x65, 0x63, 0x6b], collector().bytes )
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().ping() )
}})
it("runs the given callback on mathing pong", function() { with(this) {
var reply = null
driver().ping("Hi", function() { reply = true })
driver().parse([0x8a, 0x02, 72, 105])
assert( reply )
}})
it("does not run the callback on non-matching pong", function() { with(this) {
var reply = null
driver().ping("Hi", function() { reply = true })
driver().parse([0x8a, 0x03, 119, 97, 116])
assert( !reply )
}})
}})
describe("close", function() { with(this) {
it("writes a close frame to the socket", function() { with(this) {
driver().close("<%= reasons %>", 1003)
assertEqual( [0x88, 0x10, 0x03, 0xeb, 0x3c, 0x25, 0x3d, 0x20, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x73, 0x20, 0x25, 0x3e], collector().bytes )
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().close() )
}})
it("does not trigger the close event", function() { with(this) {
driver().close()
assertEqual( false, close )
}})
it("does not trigger the onerror event", function() { with(this) {
driver().close()
assertEqual( false, error )
}})
it("changes the state to closing", function() { with(this) {
driver().close()
assertEqual( "closing", driver().getState() )
}})
}})
}})
describe("when masking is required", function() { with(this) {
before(function() {
this.options().requireMasking = true
this.driver().start()
})
it("does not emit a message", function() { with(this) {
driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
assertEqual( "", message )
}})
it("returns an error", function() { with(this) {
driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
assertEqual( "Received unmasked frame but masking is required", error.message )
assertEqual( [1003, "Received unmasked frame but masking is required"], close )
}})
}})
describe("in the closing state", function() { with(this) {
before(function() {
this.driver().start()
this.driver().close()
})
describe("frame", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().frame("dropped")
}})
it("returns false", function() { with(this) {
assertEqual( false, driver().frame("wut") )
}})
}})
describe("ping", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().ping()
}})
it("returns false", function() { with(this) {
assertEqual( false, driver().ping() )
}})
}})
describe("close", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().close()
}})
it("returns false", function() { with(this) {
assertEqual( false, driver().close() )
}})
}})
describe("receiving a close frame", function() { with(this) {
before(function() {
this.driver().parse([0x88, 0x04, 0x03, 0xe9, 0x4f, 0x4b])
})
it("triggers the onclose event", function() { with(this) {
assertEqual( [1001, "OK"], close )
}})
it("changes the state to closed", function() { with(this) {
assertEqual( "closed", driver().getState() )
}})
}})
}})
describe("in the closed state", function() { with(this) {
before(function() {
this.driver().start()
this.driver().close()
this.driver().parse([0x88, 0x02, 0x03, 0xea])
})
describe("frame", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().frame("dropped")
}})
it("returns false", function() { with(this) {
assertEqual( false, driver().frame("wut") )
}})
}})
describe("ping", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().ping()
}})
it("returns false", function() { with(this) {
assertEqual( false, driver().ping() )
}})
}})
describe("close", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().close()
}})
it("returns false", function() { with(this) {
assertEqual( false, driver().close() )
}})
it("leaves the state as closed", function() { with(this) {
driver().close()
assertEqual( "closed", driver().getState() )
}})
}})
}})
}})