Compare commits

...

59 Commits

Author SHA1 Message Date
James Coglan 99aacf67a5 Bump copyright date. 2013-02-15 00:02:57 +00:00
James Coglan 1a6cdd3e43 Bump version to 0.4.4. 2013-02-14 22:40:48 +00:00
James Coglan 0e2417cd51 Change name used in Autobahn tests. 2013-02-14 22:39:13 +00:00
James Coglan 86f882f189 Return early rather than have an else-clause. 2013-02-10 23:41:37 +00:00
James Coglan 87f1abad08 s/close/finalize/ -- I missed one rename in the last commit. 2012-12-24 16:43:41 +00:00
James Coglan b86f505441 If WebSocket.close() is called without asking for an ack, then don't return if we're in the CLOSING state. This can happen if one peer sends a closing frame and the other peer terminates TCP before sending the reply, and as currently implemented this will no emit a close event or end the Node stream. 2012-12-24 16:37:57 +00:00
James Coglan 14a1a7372d Use progress bar using Autobahn client tests. 2012-12-23 18:25:50 +00:00
James Coglan ee22f75959 Remove trailing whitespace. 2012-12-22 23:13:04 +00:00
James Coglan 7b63baa5a6 Fix specs on v0.9. 2012-12-22 23:12:08 +00:00
James Coglan 0dc749e565 Merge pull request #18 from gsoltis/master
Fix for closing a socket in the CONNECTING state
2012-10-15 11:54:53 -07:00
Greg Soltis 3280f38c27 Fix check of readyState for immediate close 2012-10-15 11:34:44 -07:00
James Coglan 64b52a0be2 Update Node versions for Travis. 2012-10-13 17:02:07 +01:00
James Coglan bd6e89f290 If close() is called in the CONNECTING readyState, then close the TCP connection immediately without waiting for the handshake to complete. 2012-10-13 16:59:58 +01:00
James Coglan f50de64532 Update changelog. 2012-07-09 08:58:00 +01:00
James Coglan d071ec3acd Check that incoming requests have an output stream before doing anything with it. 2012-07-08 20:30:53 +01:00
James Coglan 59fd729a03 Bump version to 0.4.3. 2012-07-01 18:15:01 +01:00
James Coglan b458959e6a Update Node versions for Travis. 2012-07-01 18:12:54 +01:00
James Coglan 7f18af7c90 Use Connection:close on EventSource connections. 2012-05-21 19:41:26 +01:00
James Coglan 680d8fc759 Remove an expensive test. 2012-05-21 19:37:40 +01:00
James Coglan 90d039e371 Bump version to 0.4.2. 2012-04-06 18:57:29 +01:00
James Coglan b42cdf741d Add error code 1011. 2012-04-06 13:52:21 +01:00
James Coglan bd7c52dfa2 Use / as the default path if the client URI has no path. 2012-04-01 22:57:16 +01:00
James Coglan adee8ba51c Bump version to 0.4.1. 2012-02-26 19:00:04 +00:00
James Coglan 3675cfb798 Treat anything but a Buffer as a text message in WebSocket#send. 2012-02-25 09:49:53 +00:00
James Coglan 1222308201 Return boolean from EventSource.send(). 2012-02-16 23:15:16 +00:00
James Coglan 8c9ae84740 Treat numbers as strings when passed to Socket.send(). 2012-02-16 23:09:29 +00:00
James Coglan d404c78c46 Add items to .npmignore. 2012-02-13 12:43:12 +00:00
James Coglan a6e2fe2aaa Bump version to 0.4.0. 2012-02-13 09:15:57 +00:00
James Coglan 8f71ec1f8b Set EventSource readyState during constructor and attach pings and event listeners after sending handshake. 2012-02-13 00:26:43 +00:00
James Coglan 74b76e56b7 Only send ping messages for EventSource if the user specifies an interval, for symmetry with WebSocket. 2012-02-12 23:59:20 +00:00
James Coglan c958c5ead9 Don't send a message on connect from the server. 2012-02-12 23:54:03 +00:00
James Coglan faa1f11c28 Clean up ping code. 2012-02-11 14:06:43 +00:00
James Coglan a9659df7d8 Set readyState to OPEN as soon as possible. 2012-02-11 13:36:12 +00:00
James Coglan fe4314e62b Revert accidental change to ws.html. 2012-02-11 12:32:51 +00:00
James Coglan 8523605f88 Document return value of WebSocket.ping(). 2012-02-11 12:10:39 +00:00
James Coglan 25794989f0 Push ping logic down into HybiParser. 2012-02-11 12:07:40 +00:00
James Coglan 29cae88d2c Provide a default value (empty string) for WebSocket ping messages. 2012-02-11 12:02:04 +00:00
James Coglan e6c03b7629 Add a ping() interface to server-side WebSocket and EventSource connections. 2012-02-11 11:59:13 +00:00
James Coglan d18a818699 Do not buffer send() calls during the CONNECTING stage of WebSocket clients -- browsers throw an error in this case. 2012-02-11 11:30:28 +00:00
James Coglan a48333ee1b Node 0.5 is not available on Travis. 2012-02-11 11:23:37 +00:00
James Coglan 6de9e01e69 Buffer calls to ws.send() until the handshake is completed. 2012-02-11 11:22:28 +00:00
James Coglan 51d5978283 Pass options object to tls.connect or it hangs on Node 0.7. 2012-02-08 22:44:04 +00:00
James Coglan a2b2559bd6 Add npm test script to package.json. 2012-02-08 22:26:00 +00:00
James Coglan c3bf5e9fa8 Fix client connection error detection on Node 0.7. 2012-02-08 22:25:44 +00:00
James Coglan ce5c6015b3 Add Travis CI configuration. 2012-02-08 22:09:09 +00:00
James Coglan 8d860a83d9 Tiny formatting change. 2012-02-07 23:42:19 +00:00
James Coglan a8ab367a0f Provide usable and accurate (HAProxy-safe) onopen event on the server side. 2012-02-07 23:39:42 +00:00
James Coglan f0678e6c91 Bump version to 0.3.1. 2012-01-16 20:19:26 +00:00
James Coglan ce0f81c8d5 Call setNoDelay(true) on sockets. 2012-01-16 20:18:23 +00:00
James Coglan e2090ce019 Bump version to 0.3.0. 2012-01-13 21:41:29 +00:00
James Coglan 22a3507dad Correct EventSource error handler. 2012-01-13 21:24:54 +00:00
James Coglan 3ba69e4242 Correct variable name in WebSocket example. 2012-01-12 22:32:52 +00:00
James Coglan cc3581fd0c Reorder handlers in example server. 2012-01-12 20:18:00 +00:00
James Coglan 3488318ae6 Document the EventSource API. 2012-01-12 20:12:35 +00:00
James Coglan 91eb2642ee Store output socket as an instance variable in EventSource. 2012-01-12 19:53:06 +00:00
James Coglan 20dd83257a Implement EventSource connection handler. 2012-01-12 19:48:59 +00:00
James Coglan c82b59259f Remove some text about client versions from the README. 2012-01-09 01:52:58 +00:00
James Coglan 727aba743c Correct readme on default value of WebSocket#protocol. 2011-12-21 19:45:28 +00:00
James Coglan 0aac24bece Update repository location. 2011-12-21 17:05:33 +00:00
25 changed files with 815 additions and 377 deletions
+2
View File
@@ -1,4 +1,6 @@
.git
.gitignore
.npmignore
.redcar
.travis.yml
node_modules
+5
View File
@@ -0,0 +1,5 @@
language: node_js
node_js:
- 0.6
- 0.8
- 0.9
+39
View File
@@ -1,3 +1,42 @@
=== 0.4.4 / 2013-02-14
* Emit the 'close' event if TCP is closed before CLOSE frame is acked
=== 0.4.3 / 2012-07-09
* Add 'Connection: close' to EventSource response
* Handle situations where request.socket is undefined
=== 0.4.2 / 2012-04-06
* Add WebSocket error code 1011.
* Handle URLs with no path correctly by sending 'GET /'
=== 0.4.1 / 2012-02-26
* Treat anything other than a Buffer as a string when calling send()
=== 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
+110 -6
View File
@@ -1,5 +1,8 @@
# faye-websocket
* Travis CI build: [<img src="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/)
This is a robust, 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
@@ -7,6 +10,11 @@ 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/).
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.
The server-side socket can process [draft-75](http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75),
[draft-76](http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76),
[hybi-07](http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07)
@@ -15,7 +23,7 @@ supports both `text` and `binary` messages, and transparently handles `ping`,
`pong`, `close` and fragmented messages.
## Accepting WebSocket connections in Node
## Handling WebSocket connections in Node
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
@@ -44,13 +52,34 @@ server.addListener('upgrade', function(request, socket, head) {
server.listen(8000);
```
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 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
ws.ping('Mic check, one, two', function() {
// fires when pong is received
});
```
## Using the WebSocket client
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, though it's compatible
with servers speaking later versions of the protocol.
browser. On the wire it identifies itself as hybi-13.
```js
var WebSocket = require('faye-websocket'),
@@ -115,15 +144,90 @@ messages.
sends a text or binary message over the connection to the other peer.
* <b><tt>close(code, reason)</tt></b> closes the connection, sending the given
status code and reason text, both of which are optional.
* <b><tt>protocol</tt></b> is a string or `null` identifying the subprotocol the
socket is using.
* <b><tt>protocol</tt></b> is a string (which may be empty) identifying the
subprotocol the socket is using.
## Handling EventSource connections in Node
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.
```js
var WebSocket = require('faye-websocket'),
EventSource = WebSocket.EventSource,
http = require('http');
var server = http.createServer();
server.addListener('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.onclose = function() {
clearInterval(loop);
es = null;
};
} else {
// Normal HTTP request
response.writeHead(200, {'Content-Type': 'text/plain'});
response.write('Hello');
response.end();
}
});
server.listen(8000);
```
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`:
```js
es.send('Breaking News!', {event: 'notification', id: '99'});
```
The `EventSource` object exposes the following properties:
* <b><tt>url</tt></b> is a string containing the URL the client used to create
the EventSource.
* <b><tt>lastEventId</tt></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.
When you initialize an EventSource with ` new EventSource()`, you can pass
configuration options after the `response` parameter. Available options are:
* <b><tt>retry</tt></b> is a number that tells the client how long (in seconds)
it should wait after a dropped connection before attempting to reconnect.
* <b><tt>ping</tt></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.
For example, this creates a connection that pings every 15 seconds and is
retryable every 10 seconds if the connection is broken:
```js
var es = new EventSource(request, response, {ping: 15, retry: 10});
```
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) 2009-2011 James Coglan
Copyright (c) 2009-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
+14 -10
View File
@@ -1,40 +1,44 @@
var WebSocket = require('../lib/faye/websocket');
var WebSocket = require('../lib/faye/websocket'),
pace = require('pace');
var host = 'ws://localhost:9001',
agent = 'Faye (Node ' + process.version + ')',
agent = 'Node ' + process.version,
cases = 0,
skip = [];
var socket = new WebSocket.Client(host + '/getCaseCount');
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
socket.onclose = process.exit;
} else if (skip.indexOf(n) >= 0) {
runCase(n + 1);
} else {
console.log('Running test case #' + n + ' ...');
socket = new WebSocket.Client(host + '/runCase?case=' + n + '&agent=' + encodeURIComponent(agent));
socket.onmessage = function(event) {
socket.send(event.data);
};
socket.onclose = function() {
runCase(n + 1);
};
}
};
runCase(1);
};
+45 -16
View File
@@ -6,9 +6,49 @@ var WebSocket = require('../lib/faye/websocket'),
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.onmessage = function(event) {
ws.send(event.data);
};
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);
es.send('Welcome!\n\nThis is an EventSource server.');
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'});
@@ -22,20 +62,9 @@ var server = secure
key: fs.readFileSync(__dirname + '/../spec/server.key'),
cert: fs.readFileSync(__dirname + '/../spec/server.crt')
})
: http.createServer(staticHandler);
server.addListener('upgrade', function(request, socket, head) {
var ws = new WebSocket(request, socket, head, ['irc', 'xmpp']);
console.log('open', ws.url, ws.version, ws.protocol);
ws.onmessage = function(event) {
ws.send(event.data);
};
ws.onclose = function(event) {
console.log('close', event.code, event.reason);
ws = null;
};
});
: http.createServer();
server.addListener('request', requestHandler);
server.addListener('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 -17
View File
@@ -5,36 +5,40 @@
<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;
socket.onopen = function() {
logger.innerHTML += '<li>OPEN: ' + socket.protocol + '</li>';
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) {
logger.innerHTML += '<li>ERROR: ' + error.message + '</li>';
};
socket.addEventListener('message', function(event) {
logger.innerHTML += '<li>MESSAGE: ' + event.data + '</li>';
setTimeout(function() { socket.send(++index + ' ' + event.data) }, 2000);
});
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) {
logger.innerHTML += '<li>CLOSE: ' + event.code + ', ' + event.reason + '</li>';
log('CLOSE: ' + event.code + ', ' + event.reason);
};
</script>
</body>
</html>
+110
View File
@@ -0,0 +1,110 @@
var API = require('./websocket/api'),
Event = require('./websocket/api/event');
var isSecureConnection = function(request) {
if (request.headers['x-forwarded-proto']) {
return request.headers['x-forwarded-proto'] === 'https';
} else {
return (request.connection && request.connection.authorized !== undefined) ||
(request.socket && request.socket.secure);
}
};
var EventSource = function(request, response, options) {
options = options || {};
this._request = request;
this._response = response;
this._stream = response.socket;
this._ping = options.ping || this.DEFAULT_PING;
this._retry = options.retry || this.DEFAULT_RETRY;
var scheme = isSecureConnection(request) ? 'https:' : 'http:';
this.url = scheme + '//' + request.headers.host + request.url;
this.lastEventId = request.headers['last-event-id'] || '';
var self = this;
this.readyState = API.CONNECTING;
this._sendBuffer = [];
process.nextTick(function() { self._open() });
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.readyState = API.OPEN;
if (this._ping)
this._pingLoop = setInterval(function() { self.ping() }, this._ping * 1000);
if (!this._stream || !this._stream.writable) return;
this._stream.setTimeout(0);
this._stream.setNoDelay(true);
try { this._stream.write(handshake, 'utf8') } catch (e) {}
['close', 'end', 'error'].forEach(function(event) {
self._stream.addListener(event, function() { self.close() });
});
};
EventSource.isEventSource = function(request) {
var accept = (request.headers.accept || '').split(/\s*,\s*/);
return accept.indexOf('text/event-stream') >= 0;
};
var instance = {
DEFAULT_PING: 10,
DEFAULT_RETRY: 5,
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';
try {
this._stream.write(frame, 'utf8');
return true;
} catch (e) {
return false;
}
},
ping: function() {
try {
this._stream.write(':\r\n\r\n', 'utf8');
return true;
} catch (e) {
return false;
}
},
close: function() {
if (this.readyState === API.CLOSING || this.readyState === API.CLOSED)
return;
this.readyState = API.CLOSED;
clearInterval(this._pingLoop);
this._response.end();
var event = new Event('close');
event.initEvent('close', false, false);
this.dispatchEvent(event);
}
};
for (var key in API) EventSource.prototype[key] = API[key];
for (var key in instance) EventSource.prototype[key] = instance[key];
module.exports = EventSource;
+41 -21
View File
@@ -10,7 +10,8 @@
var Draft75Parser = require('./websocket/draft75_parser'),
Draft76Parser = require('./websocket/draft76_parser'),
HybiParser = require('./websocket/hybi_parser'),
API = require('./websocket/api');
API = require('./websocket/api'),
Event = require('./websocket/api/event');
var getParser = function(request) {
var headers = request.headers;
@@ -30,44 +31,63 @@ var isSecureConnection = function(request) {
}
};
var WebSocket = function(request, socket, head, supportedProtos) {
var WebSocket = function(request, socket, head, supportedProtos, options) {
this.request = request;
this._stream = request.socket;
this._ping = options && options.ping;
this._pingId = 0;
var scheme = isSecureConnection(request) ? 'wss:' : 'ws:';
this.url = scheme + '//' + request.headers.host + request.url;
this.url = scheme + '//' + request.headers.host + request.url;
this.readyState = API.CONNECTING;
this.bufferedAmount = 0;
var Parser = getParser(request);
this._parser = new Parser(this, {protocols: supportedProtos});
var handshake = this._parser.handshakeResponse(head);
try { this._stream.write(handshake, 'binary') } catch (e) {}
this.protocol = this._parser.protocol || '';
this.readyState = API.OPEN;
this.version = this._parser.getVersion();
var event = new API.Event('open');
event.initEvent('open', false, false);
this.dispatchEvent(event);
var self = this;
this._sendBuffer = [];
process.nextTick(function() { self._open() });
var handshake = this._parser.handshakeResponse(head);
if (this._parser.isOpen()) this.readyState = API.OPEN;
if (this._ping)
this._pingLoop = setInterval(function() {
self._pingId += 1;
self.ping(self._pingId.toString());
}, this._ping * 1000);
this.protocol = this._parser.protocol || '';
this.version = this._parser.getVersion();
if (!this._stream || !this._stream.writable) return;
this._stream.setTimeout(0);
this._stream.setNoDelay(true);
try { this._stream.write(handshake, 'binary') } catch (e) {}
this._stream.addListener('data', function(data) {
var response = self._parser.parse(data);
if (!response) return;
try { self._stream.write(response, 'binary') } catch (e) {}
self._open();
});
['close', 'end', 'error'].forEach(function(event) {
self._stream.addListener(event, function() { self.close(1006, '', false) });
});
};
var API = require('./websocket/api');
WebSocket.prototype.ping = function(message, callback, context) {
if (!this._parser.ping) return false;
return this._parser.ping(message, callback, context);
};
for (var key in API) WebSocket.prototype[key] = API[key];
WebSocket.Client = require('./websocket/client');
module.exports = WebSocket;
WebSocket.WebSocket = WebSocket;
WebSocket.Client = require('./websocket/client');
WebSocket.EventSource = require('./eventsource');
module.exports = WebSocket;
+55 -82
View File
@@ -1,25 +1,50 @@
var EventTarget = require('./api/event_target'),
Event = require('./api/event');
var API = {
CONNECTING: 0,
OPEN: 1,
CLOSING: 2,
CLOSED: 3,
onopen: null,
onmessage: null,
onerror: null,
onclose: null,
protocol: null,
_open: function() {
if (this._parser && !this._parser.isOpen()) return;
this.readyState = API.OPEN;
var buffer = this._sendBuffer || [],
message;
while (message = buffer.shift())
this.send.apply(this, message);
var event = new Event('open');
event.initEvent('open', false, false);
this.dispatchEvent(event);
},
receive: function(data) {
if (this.readyState !== API.OPEN) return false;
var event = new API.Event('message');
var event = new Event('message');
event.initEvent('message', false, false);
event.data = data;
this.dispatchEvent(event);
},
send: function(data, type, errorType) {
if (this.readyState === API.CLOSED) return false;
if (this.readyState === API.CONNECTING) {
if (this._sendBuffer) {
this._sendBuffer.push(arguments);
return true;
} else {
throw new Error('Cannot call send(), socket is not open yet');
}
}
if (this.readyState === API.CLOSED)
return false;
if (!(data instanceof Buffer)) data = String(data);
var frame = this._parser.frame(data, type, errorType);
try {
this._stream.write(frame, 'binary');
@@ -28,88 +53,36 @@ var API = {
return false;
}
},
close: function(code, reason, ack) {
if (this.readyState === API.CLOSING ||
this.readyState === API.CLOSED) return;
this.readyState = API.CLOSING;
var close = function() {
if (this.readyState === API.CLOSED) return;
if (this.readyState === API.CLOSING && ack !== false) return;
var finalize = function() {
this.readyState = API.CLOSED;
this._stream.end();
var event = new API.Event('close', {code: code || 1000, reason: reason || ''});
if (this._pingLoop) clearInterval(this._pingLoop);
if (this._stream) this._stream.end();
var event = new Event('close', {code: code || 1000, reason: reason || ''});
event.initEvent('close', false, false);
this.dispatchEvent(event);
};
if (ack !== false) {
if (this._parser.close) this._parser.close(code, reason, close, this);
else close.call(this);
} else {
if (this.readyState === API.CONNECTING)
return finalize.call(this);
this.readyState = API.CLOSING;
if (ack === false) {
if (this._parser.close) this._parser.close(code, reason);
close.call(this);
finalize.call(this);
} else {
if (this._parser.close) this._parser.close(code, reason, finalize, this);
else finalize.call(this);
}
},
addEventListener: function(eventType, listener, useCapture) {
this._listeners = this._listeners || {};
var list = this._listeners[eventType] = this._listeners[eventType] || [];
list.push(listener);
},
removeEventListener: function(eventType, listener, useCapture) {
if (!this._listeners || !this._listeners[eventType]) return;
if (!listener) {
delete this._listeners[eventType];
return;
}
var list = this._listeners[eventType],
i = list.length;
while (i--) {
if (listener !== list[i]) continue;
list.splice(i,1);
}
},
dispatchEvent: function(event) {
event.target = event.currentTarget = this;
event.eventPhase = API.Event.AT_TARGET;
if (this['on' + event.type])
this['on' + event.type](event);
if (!this._listeners || !this._listeners[event.type]) return;
this._listeners[event.type].forEach(function(listener) {
listener(event);
}, this);
}
};
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;
API.Event = Event;
for (var key in EventTarget) API[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;
+47
View File
@@ -0,0 +1,47 @@
var Event = require('./event');
var EventTarget = {
onopen: null,
onmessage: null,
onerror: null,
onclose: null,
addEventListener: function(eventType, listener, useCapture) {
this._listeners = this._listeners || {};
var list = this._listeners[eventType] = this._listeners[eventType] || [];
list.push(listener);
},
removeEventListener: function(eventType, listener, useCapture) {
if (!this._listeners || !this._listeners[eventType]) return;
if (!listener) {
delete this._listeners[eventType];
return;
}
var list = this._listeners[eventType],
i = list.length;
while (i--) {
if (listener !== list[i]) continue;
list.splice(i,1);
}
},
dispatchEvent: function(event) {
event.target = event.currentTarget = this;
event.eventPhase = Event.AT_TARGET;
if (this['on' + event.type])
this['on' + event.type](event);
if (!this._listeners || !this._listeners[event.type]) return;
this._listeners[event.type].forEach(function(listener) {
listener(event);
}, this);
}
};
module.exports = EventTarget;
+29 -23
View File
@@ -1,39 +1,45 @@
var API = require('./api'),
net = require('net'),
var net = require('net'),
tls = require('tls');
var HybiParser = require('./hybi_parser');
var HybiParser = require('./hybi_parser'),
API = require('./api'),
Event = require('./api/event');
var Client = function(url, protocols) {
var Client = function(url, protocols, options) {
this.url = url;
this._uri = require('url').parse(url);
this.protocol = '';
this.readyState = API.CONNECTING;
this.bufferedAmount = 0;
var secure = (this._uri.protocol === 'wss:'),
self = this,
onConnect = function() { self._onConnect() },
connection = secure
? tls.connect(this._uri.port || 443, this._uri.hostname, onConnect)
tlsOptions = {};
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._parser = new HybiParser(this, {masking: true, protocols: protocols});
this._stream = connection;
this._stream.setTimeout(0);
this._stream.setNoDelay(true);
if (!secure) connection.addListener('connect', onConnect);
connection.addListener('data', function(data) {
self._onData(data);
});
connection.addListener('close', function() {
self.close(1006, '', false);
['close', 'end', 'error'].forEach(function(event) {
connection.addListener(event, function() { self.close(1006, '', false) });
});
connection.addListener('error', function() {});
};
Client.prototype._onConnect = function() {
this._handshake = this._parser.createHandshake(this._uri, this._stream);
this._message = [];
@@ -48,26 +54,26 @@ Client.prototype._onData = function(data) {
var bytes = this._handshake.parse(data);
for (var i = 0, n = bytes.length; i < n; i++)
this._message.push(bytes[i]);
if (!this._handshake.isComplete()) return;
if (this._handshake.isValid()) {
this.protocol = this._handshake.protocol || '';
this.readyState = API.OPEN;
var event = new API.Event('open');
var event = new Event('open');
event.initEvent('open', false, false);
this.dispatchEvent(event);
this._parser.parse(this._message);
} else {
this.readyState = API.CLOSED;
var event = new API.Event('close', {code: 1006, reason: ''});
var event = new Event('close', {code: 1006, reason: ''});
event.initEvent('close', false, false);
this.dispatchEvent(event);
}
break;
case API.OPEN:
case API.CLOSING:
this._parser.parse(data);
+15 -11
View File
@@ -7,7 +7,7 @@ var instance = {
getVersion: function() {
return 'hixie-75';
},
handshakeResponse: function() {
return new Buffer('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
@@ -16,21 +16,25 @@ var instance = {
'WebSocket-Location: ' + this._socket.url + '\r\n\r\n',
'utf8');
},
isOpen: function() {
return true;
},
parse: function(buffer) {
var data, message, value;
for (var i = 0, n = buffer.length; i < n; i++) {
data = buffer[i];
switch (this._stage) {
case 0:
this._parseLeadingByte(data);
break;
case 1:
value = (data & 0x7F);
this._length = value + 128 * this._length;
if (this._closing && this._length === 0) {
this._socket.close(null, null, false);
}
@@ -45,7 +49,7 @@ var instance = {
}
}
break;
case 2:
if (data === 0xFF) {
message = new Buffer(this._buffer);
@@ -61,7 +65,7 @@ var instance = {
}
}
},
_parseLeadingByte: function(data) {
if ((0x80 & data) === 0x80) {
this._length = 0;
@@ -72,17 +76,17 @@ var instance = {
this._stage = 2;
}
},
frame: function(data) {
if (Buffer.isBuffer(data)) return data;
var buffer = new Buffer(data, 'utf8'),
frame = new Buffer(buffer.length + 2);
frame[0] = 0x00;
frame[buffer.length + 1] = 0xFF;
buffer.copy(frame, 1);
return frame;
}
};
+17 -13
View File
@@ -25,17 +25,17 @@ var bigEndian = function(number) {
Draft76Parser.prototype.getVersion = function() {
return 'hixie-76';
};
Draft76Parser.prototype.handshakeResponse = function(head) {
var request = this._socket.request, tmp;
var response = new Buffer('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' +
'Sec-WebSocket-Origin: ' + request.headers.origin + '\r\n' +
'Sec-WebSocket-Location: ' + this._socket.url + '\r\n\r\n',
'binary');
var signature = this.handshakeSignature(head);
if (signature) {
tmp = new Buffer(response.length + signature.length);
@@ -43,42 +43,46 @@ Draft76Parser.prototype.handshakeResponse = function(head) {
signature.copy(tmp, response.length);
response = tmp;
}
return response;
};
Draft76Parser.prototype.isOpen = function() {
return !!this._handshakeComplete;
};
Draft76Parser.prototype.handshakeSignature = function(head) {
if (head.length === 0) return null;
this._handshakeComplete = true;
var request = this._socket.request,
key1 = request.headers['sec-websocket-key1'],
value1 = numberFromKey(key1) / spacesInKey(key1),
key2 = request.headers['sec-websocket-key2'],
value2 = numberFromKey(key2) / spacesInKey(key2),
MD5 = crypto.createHash('md5');
MD5.update(bigEndian(value1));
MD5.update(bigEndian(value2));
MD5.update(head.toString('binary'));
this._handshakeComplete = true;
return new Buffer(MD5.digest('binary'), 'binary');
};
Draft76Parser.prototype.parse = function(data) {
if (this._handshakeComplete)
return Draft75Parser.prototype.parse.call(this, data);
return this.handshakeSignature(data);
};
Draft76Parser.prototype._parseLeadingByte = function(data) {
if (data !== 0xFF)
return Draft75Parser.prototype._parseLeadingByte.call(this, data);
this._closing = true;
this._length = 0;
this._stage = 1;
+78 -57
View File
@@ -9,7 +9,9 @@ var HybiParser = function(webSocket, options) {
this._stage = 0;
this._masking = options && options.masking;
this._protocols = options && options.protocols;
this._pingCallbacks = {};
if (typeof this._protocols === 'string')
this._protocols = this._protocols.split(/\s*,\s*/);
};
@@ -17,7 +19,7 @@ var HybiParser = function(webSocket, options) {
HybiParser.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];
}
@@ -33,7 +35,7 @@ var instance = {
RSV3: 16,
OPCODE: 15,
LENGTH: 127,
OPCODES: {
continuation: 0,
text: 1,
@@ -42,49 +44,50 @@ var instance = {
ping: 9,
pong: 10
},
ERRORS: {
normal_closure: 1000,
going_away: 1001,
protocol_error: 1002,
unacceptable: 1003,
encoding_error: 1007,
policy_violation: 1008,
too_large: 1009,
extension_error: 1010
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
},
FRAGMENTED_OPCODES: [0,1,2],
OPENING_OPCODES: [1,2],
ERROR_CODES: [1000,1001,1002,1003,1007,1008,1009,1010],
ERROR_CODES: [1000,1001,1002,1003,1007,1008,1009,1010,1011],
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})*$/,
getVersion: function() {
var version = this._socket.request.headers['sec-websocket-version'];
return 'hybi-' + version;
},
handshakeResponse: function() {
var secKey = this._socket.request.headers['sec-websocket-key'];
if (!secKey) return null;
var SHA1 = crypto.createHash('sha1');
SHA1.update(secKey + Handshake.GUID);
var accept = SHA1.digest('base64'),
protos = this._socket.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 && supported !== undefined) {
if (typeof protos === 'string') protos = protos.split(/\s*,\s*/);
proto = protos.filter(function(p) { return supported.indexOf(p) >= 0 })[0];
@@ -93,14 +96,18 @@ var instance = {
headers.push('Sec-WebSocket-Protocol: ' + proto);
}
}
return new Buffer(headers.concat('','').join('\r\n'), 'utf8');
},
isOpen: function() {
return true;
},
createHandshake: function(uri) {
return new Handshake(uri, this._protocols);
},
parse: function(data) {
this._reader.put(data);
var buffer = true;
@@ -110,17 +117,17 @@ var instance = {
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) {
@@ -128,7 +135,7 @@ var instance = {
this._stage = 4;
}
break;
case 4:
buffer = this._reader.read(this._length);
if (buffer) {
@@ -140,40 +147,40 @@ var instance = {
}
}
},
_parseOpcode: function(data) {
var rsvs = [this.RSV1, this.RSV2, this.RSV3].filter(function(rsv) {
return (data & rsv) === rsv;
}, this);
if (rsvs.length > 0) return this._socket.close(this.ERRORS.protocol_error, null, false);
this._final = (data & this.FIN) === this.FIN;
this._opcode = (data & this.OPCODE);
this._mask = [];
this._payload = [];
var valid = false;
for (var key in this.OPCODES) {
if (this.OPCODES[key] === this._opcode)
valid = true;
}
if (!valid) return this._socket.close(this.ERRORS.protocol_error, null, false);
if (this.FRAGMENTED_OPCODES.indexOf(this._opcode) < 0 && !this._final)
return this._socket.close(this.ERRORS.protocol_error, null, false);
if (this._mode && this.OPENING_OPCODES.indexOf(this._opcode) >= 0)
return this._socket.close(this.ERRORS.protocol_error, null, false);
this._stage = 1;
},
_parseLength: function(data) {
this._masked = (data & this.MASK) === this.MASK;
this._length = (data & this.LENGTH);
if (this._length >= 0 && this._length <= 125) {
this._stage = this._masked ? 3 : 4;
} else {
@@ -182,15 +189,15 @@ var instance = {
this._stage = 2;
}
},
_parseExtendedLength: function(buffer) {
this._length = this._getInteger(buffer);
this._stage = this._masked ? 3 : 4;
},
frame: function(data, type, code) {
if (this._closed) return null;
var isText = (typeof data === 'string'),
opcode = this.OPCODES[type || (isText ? 'text' : 'binary')],
buffer = isText ? new Buffer(data, 'utf8') : data,
@@ -202,9 +209,9 @@ var instance = {
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) {
@@ -222,39 +229,45 @@ var instance = {
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);
HybiParser.mask(frame, mask, offset);
}
return frame;
},
ping: function(message, callback, context) {
message = message || '';
if (callback) this._pingCallbacks[message] = [callback, context];
return this._socket.send(message, 'ping');
},
close: function(code, reason, callback, context) {
if (this._closed) return;
if (callback) this._closingCallback = [callback, context];
this._socket.send(reason || '', 'close', code || this.ERRORS.normal_closure);
this._closed = true;
},
buffer: function(fragment) {
for (var i = 0, n = fragment.length; i < n; i++)
this._buffer.push(fragment[i]);
},
_emitFrame: function() {
var payload = HybiParser.mask(this._payload, this._mask),
opcode = this._opcode;
if (opcode === this.OPCODES.continuation) {
if (!this._mode) return this._socket.close(this.ERRORS.protocol_error, null, false);
this.buffer(payload);
@@ -287,15 +300,15 @@ var instance = {
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 >= 3000 && code < 5000) &&
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._socket.close(code, (payload.length > 2) ? reason : null, false);
if (this._closingCallback)
this._closingCallback[0].call(this._closingCallback[1]);
@@ -304,13 +317,21 @@ var instance = {
if (payload.length > 125) return this._socket.close(this.ERRORS.protocol_error, null, false);
this._socket.send(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[0].call(callback[1]);
}
},
_reset: function() {
this._mode = null;
this._buffer = [];
},
_encode: function(buffer) {
try {
var string = buffer.toString('binary', 0, buffer.length);
@@ -318,7 +339,7 @@ var instance = {
} 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++)
+14 -14
View File
@@ -3,25 +3,25 @@ var crypto = require('crypto');
var Handshake = function(uri, protocols) {
this._uri = uri;
this._protocols = protocols;
var buffer = new Buffer(16), i = 16;
while (i--) buffer[i] = Math.floor(Math.random() * 256);
this._key = buffer.toString('base64');
var SHA1 = crypto.createHash('sha1');
SHA1.update(this._key + Handshake.GUID);
this._accept = SHA1.digest('base64');
var HTTPParser = process.binding('http_parser').HTTPParser,
parser = new HTTPParser(HTTPParser.RESPONSE || 'response'),
current = null,
self = this;
this._nodeVersion = HTTPParser.RESPONSE ? 6 : 4;
this._complete = false;
this._headers = {};
this._parser = parser;
parser.onHeaderField = function(b, start, length) {
current = b.toString('utf8', start, start + length);
};
@@ -41,29 +41,29 @@ var Handshake = function(uri, protocols) {
};
Handshake.GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
Handshake.prototype.requestData = function() {
var u = this._uri;
var headers = [
'GET ' + u.pathname + (u.search || '') + ' HTTP/1.1',
'GET ' + (u.pathname || '/') + (u.search || '') + ' HTTP/1.1',
'Host: ' + u.hostname + (u.port ? ':' + u.port : ''),
'Upgrade: websocket',
'Connection: Upgrade',
'Sec-WebSocket-Key: ' + this._key,
'Sec-WebSocket-Version: 13'
];
if (this._protocols)
headers.push('Sec-WebSocket-Protocol: ' + this._protocols.join(', '));
return new Buffer(headers.concat('','').join('\r\n'), 'utf8');
};
Handshake.prototype.parse = function(data) {
var consumed = this._parser.execute(data, 0, data.length),
offset = (this._nodeVersion < 6) ? 1 : 0;
return (consumed === data.length) ? [] : data.slice(consumed + offset);
};
@@ -73,15 +73,15 @@ Handshake.prototype.isComplete = function() {
Handshake.prototype.isValid = function() {
if (this._status !== 101) return false;
var upgrade = this._headers.Upgrade,
connection = this._headers.Connection,
protocol = this._headers['Sec-WebSocket-Protocol'];
this.protocol = this._protocols && this._protocols.indexOf(protocol) >= 0
? protocol
: null;
return upgrade && /^websocket$/i.test(upgrade) &&
connection && connection.split(/\s*,\s*/).indexOf('Upgrade') >= 0 &&
((!this._protocols && !protocol) || this.protocol) &&
@@ -20,9 +20,9 @@ StreamReader.prototype._readBuffer = function(length) {
n = queue.length,
i = 0,
chunk, offset, size;
if (remain === 0) return buffer;
while (remain > 0 && i < n) {
chunk = queue[i];
offset = (i === 0) ? this._cursor : 0;
@@ -31,12 +31,12 @@ StreamReader.prototype._readBuffer = function(length) {
remain -= size;
i += 1;
}
if (remain > 0) return null;
queue.splice(0, i-1);
this._cursor = (i === 1 ? this._cursor : 0) + size;
return buffer;
};
+11 -9
View File
@@ -1,23 +1,25 @@
{ "name" : "faye-websocket"
, "description" : "Standards-compliant WebSocket server and client"
, "homepage" : "http://github.com/jcoglan/faye-websocket-node"
, "homepage" : "http://github.com/faye/faye-websocket-node"
, "author" : "James Coglan <jcoglan@gmail.com> (http://jcoglan.com/)"
, "keywords" : ["websocket"]
, "keywords" : ["websocket", "eventsource"]
, "version" : "0.2.0"
, "version" : "0.4.4"
, "engines" : {"node": ">=0.4.0"}
, "main" : "./lib/faye/websocket"
, "devDependencies" : {"jsclass": ""}
, "devDependencies" : {"jsclass": "", "pace": ""}
, "bugs" : "http://github.com/jcoglan/faye-websocket-node/issues"
, "scripts" : {"test": "node spec/runner.js"}
, "licenses" : [ { "type" : "MIT"
, "url" : "http://www.opensource.org/licenses/mit-license.php"
, "bugs" : "http://github.com/faye/faye-websocket-node/issues"
, "licenses" : [ { "type" : "MIT"
, "url" : "http://www.opensource.org/licenses/mit-license.php"
}
]
, "repositories" : [ { "type" : "git"
, "url" : "git://github.com/jcoglan/faye-websocket-node.git"
, "repositories" : [ { "type" : "git"
, "url" : "git://github.com/faye/faye-websocket-node.git"
}
]
}
+53 -40
View File
@@ -7,29 +7,29 @@ JS.ENV.WebSocketSteps = JS.Test.asyncSteps({
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)
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() {
@@ -38,38 +38,38 @@ JS.ENV.WebSocketSteps = JS.Test.asyncSteps({
}
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(callback) {
this._ws.send("I expect this to be echoed")
send_message: function(message, callback) {
this._ws.send(message)
setTimeout(callback, 100)
},
check_response: function(callback) {
this.assertEqual( "I expect this to be echoed", this._message )
check_response: function(message, callback) {
this.assertEqual( message, this._message )
callback()
},
check_no_response: function(callback) {
this.assert( !this._message )
callback()
@@ -79,83 +79,96 @@ JS.ENV.WebSocketSteps = JS.Test.asyncSteps({
JS.ENV.ClientSpec = JS.Test.describe("Client", function() { with(this) {
include(WebSocketSteps)
before(function() {
this.protocols = ["foo", "echo"]
this.plain_text_url = "ws://localhost:8000/bayeux"
this.secure_url = "wss://localhost:8000/bayeux"
})
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("cannot open a connection to the wrong host", function() { with(this) {
open_socket(blocked_url, protocols)
check_closed()
}})
it("cannot open a connection with unacceptable protocols", function() { with(this) {
open_socket(socket_url, ["foo"])
check_closed()
}})
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()
check_response()
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()
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(8000, 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(8000, true) })
after (function() { this.stop() })
behavesLike("socket client")
}})
}})
+12 -12
View File
@@ -5,66 +5,66 @@ JS.ENV.Draft75ParserSpec = JS.Test.describe("Draft75Parser", function() { with(t
this.webSocket = {dispatchEvent: function() {}}
this.parser = new Draft75Parser(webSocket)
}})
describe("parse", function() { with(this) {
sharedBehavior("draft-75 parser", function() { with(this) {
it("parses text frames", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parser.parse([0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff])
}})
it("parses multiple frames from the same packet", function() { with(this) {
expect(webSocket, "receive").given("Hello").exactly(2)
parser.parse([0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff])
}})
it("parses text frames beginning 0x00-0x7F", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parser.parse([0x66, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff])
}})
it("ignores frames with a length header", function() { with(this) {
expect(webSocket, "receive").exactly(0)
parser.parse([0x80, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
}})
it("parses text following an ignored block", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parser.parse([0x80, 0x02, 0x48, 0x65, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff])
}})
it("parses multibyte text frames", function() { with(this) {
expect(webSocket, "receive").given("Apple = ")
parser.parse([0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff])
}})
it("parses frames received in several packets", function() { with(this) {
expect(webSocket, "receive").given("Apple = ")
parser.parse([0x00, 0x41, 0x70, 0x70, 0x6c, 0x65])
parser.parse([0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff])
}})
it("parses fragmented frames", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parser.parse([0x00, 0x48, 0x65, 0x6c])
parser.parse([0x6c, 0x6f, 0xff])
}})
}})
behavesLike("draft-75 parser")
it("does not close the socket if a 76 close frame is received", function() { with(this) {
expect(webSocket, "close").exactly(0)
expect(webSocket, "receive").given("")
parser.parse([0xFF, 0x00])
}})
}})
describe("frame", function() { with(this) {
it("returns the given string formatted as a WebSocket frame", function() { with(this) {
assertBufferEqual( [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff], parser.frame("Hello") )
}})
it("encodes multibyte characters correctly", function() { with(this) {
assertBufferEqual( [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff], parser.frame("Apple = ") )
}})
+4 -4
View File
@@ -6,21 +6,21 @@ JS.ENV.Draft76ParserSpec = JS.Test.describe("Draft76Parser", function() { with(t
this.parser = new Draft76Parser(webSocket)
parser._handshakeComplete = true
}})
describe("parse", function() { with(this) {
behavesLike("draft-75 parser")
it("closes the socket if a close frame is received", function() { with(this) {
expect(webSocket, "close")
parser.parse([0xFF, 0x00])
}})
}})
describe("frame", function() { with(this) {
it("returns the given string formatted as a WebSocket frame", function() { with(this) {
assertBufferEqual( [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff], parser.frame("Hello") )
}})
it("encodes multibyte characters correctly", function() { with(this) {
assertBufferEqual( [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff], parser.frame("Apple = ") )
}})
+26 -35
View File
@@ -5,13 +5,13 @@ JS.ENV.HybiParserSpec = JS.Test.describe("HybiParser", function() { with(this) {
this.webSocket = {dispatchEvent: function() {}}
this.parser = new HybiParser(webSocket)
}})
define("parse", function() {
var bytes = [];
for (var i = 0, n = arguments.length; i < n; i++) bytes = bytes.concat(arguments[i])
this.parser.parse(new Buffer(bytes))
})
define("buffer", function(string) {
return {
equals: function(buffer) {
@@ -19,12 +19,12 @@ JS.ENV.HybiParserSpec = JS.Test.describe("HybiParser", function() { with(this) {
}
}
})
describe("parse", function() { with(this) {
define("mask", function() {
return this._mask = this._mask || [1,2,3,4].map(function() { return Math.floor(Math.random() * 255) })
})
define("maskMessage", function(bytes) {
var output = []
Array.prototype.forEach.call(bytes, function(b, i) {
@@ -32,124 +32,115 @@ JS.ENV.HybiParserSpec = JS.Test.describe("HybiParser", function() { with(this) {
}, this)
return output
})
it("parses unmasked text frames", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
}})
it("parses multiple frames from the same packet", function() { with(this) {
expect(webSocket, "receive").given("Hello").exactly(2)
parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
}})
it("parses empty text frames", function() { with(this) {
expect(webSocket, "receive").given("")
parse([0x81, 0x00])
}})
it("parses fragmented text frames", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parse([0x01, 0x03, 0x48, 0x65, 0x6c])
parse([0x80, 0x02, 0x6c, 0x6f])
}})
it("parses masked text frames", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parse([0x81, 0x85], mask(), maskMessage([0x48, 0x65, 0x6c, 0x6c, 0x6f]))
}})
it("parses masked empty text frames", function() { with(this) {
expect(webSocket, "receive").given("")
parse([0x81, 0x80], mask(), maskMessage([]))
}})
it("parses masked fragmented text frames", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parse([0x01, 0x81], mask(), maskMessage([0x48]))
parse([0x80, 0x84], mask(), maskMessage([0x65, 0x6c, 0x6c, 0x6f]))
}})
it("closes the socket if the frame has an unrecognized opcode", function() { with(this) {
expect(webSocket, "close").given(1002, null, false)
parse([0x83, 0x00])
}})
it("closes the socket if a close frame is received", function() { with(this) {
expect(webSocket, "close").given(1000, "Hello", false)
parse([0x88, 0x07, 0x03, 0xe8, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
}})
it("parses unmasked multibyte text frames", function() { with(this) {
expect(webSocket, "receive").given("Apple = ")
parse([0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf])
}})
it("parses frames received in several packets", function() { with(this) {
expect(webSocket, "receive").given("Apple = ")
parse([0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c])
parse([0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf])
}})
it("parses fragmented multibyte text frames", function() { with(this) {
expect(webSocket, "receive").given("Apple = ")
parse([0x01, 0x0a, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3])
parse([0x80, 0x01, 0xbf])
}})
it("parses masked multibyte text frames", function() { with(this) {
expect(webSocket, "receive").given("Apple = ")
parse([0x81, 0x8b], mask(), maskMessage([0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf]))
}})
it("parses masked fragmented multibyte text frames", function() { with(this) {
expect(webSocket, "receive").given("Apple = ")
parse([0x01, 0x8a], mask(), maskMessage([0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3]))
parse([0x80, 0x81], mask(), maskMessage([0xbf]))
}})
it("parses unmasked medium-length text frames", function() { with(this) {
expect(webSocket, "receive").given("HelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHello")
parse([129, 126, 0, 200, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111])
}})
it("parses masked medium-length text frames", function() { with(this) {
expect(webSocket, "receive").given("HelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHello")
parse([129, 254, 0, 200], mask(), maskMessage([72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111]))
}})
it("replies to pings with a pong", function() { with(this) {
expect(webSocket, "send").given(buffer("OHAI"), "pong")
parse([0x89, 0x04, 0x4f, 0x48, 0x41, 0x49])
}})
}})
describe("frame", function() { with(this) {
it("returns the given string formatted as a WebSocket frame", function() { with(this) {
assertBufferEqual( [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f], parser.frame("Hello") )
}})
it("encodes multibyte characters correctly", function() { with(this) {
assertBufferEqual( [0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf], parser.frame("Apple = ") )
}})
it("encodes medium-length strings using extra length bytes", function() { with(this) {
assertBufferEqual( [129, 126, 0, 200, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111], parser.frame("HelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHello") )
}})
it("encodes long strings using extra length bytes", function() { with(this) {
var reps = 13108, message = '', output = [0x81, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x04]
while (reps--) {
message += "Hello"
output = output.concat([0x48, 0x65, 0x6c, 0x6c, 0x6f])
}
assertBufferEqual( output, parser.frame(message) )
}})
it("encodes close frames with an error code", function() { with(this) {
assertBufferEqual( [0x88, 0x07, 0x03, 0xea, 0x48, 0x65, 0x6c, 0x6c, 0x6f], parser.frame("Hello", "close", 1002) )
}})
it("encodes pong frames", function() { with(this) {
assertBufferEqual( [0x8a, 0x00], parser.frame("", "pong") )
}})
+2 -2
View File
@@ -14,7 +14,7 @@ EchoServer.prototype.listen = function(port, ssl) {
cert: fs.readFileSync(__dirname + '/server.crt')
})
: http.createServer()
server.addListener('upgrade', function(request, socket, head) {
var ws = new WebSocket(request, socket, head, ["echo"])
ws.onmessage = function(event) {
@@ -44,7 +44,7 @@ JS.require('JS.Test', function() {
while (n--) ary[n] = buffer[n];
this.assertEqual(array, ary);
})
JS.require( 'ClientSpec',
'Draft75ParserSpec',
'Draft76ParserSpec',