Compare commits

..

6 Commits

Author SHA1 Message Date
Guillermo Rauch
64d8f572aa Release 0.9.15 2013-06-06 08:22:29 -07:00
Guillermo Rauch
4e1ba9f872 transports: escaping (fixes #1251) 2013-06-06 08:17:40 -07:00
Guillermo Rauch
5d93af994a Release 0.9.14 2013-03-29 14:15:52 -07:00
Guillermo Rauch
6e25c802cc manager: fix memory leak with SSL 2013-03-29 14:14:42 -07:00
Guillermo Rauch
37690f78d7 Release 0.9.13 2012-12-13 15:15:38 -03:00
Guillermo Rauch
3cbd00ca70 package: fixed base64id requirement 2012-12-13 15:15:15 -03:00
89 changed files with 18015 additions and 3247 deletions

1
.gitignore vendored
View File

@@ -8,4 +8,3 @@ lib-cov
*.pid
benchmarks/*.png
node_modules
coverage

View File

@@ -1,13 +1,6 @@
language: node_js
node_js:
- "0.8"
- "0.10"
- "0.11"
matrix:
fast_finish: true
allow_failures:
- node_js: "0.11"
- 0.6
notifications:
irc: "irc.freenode.org#socket.io"

View File

@@ -1,50 +1,8 @@
1.0.0 / 2014-05-28
==================
0.9.15 / 2013-06-06
===================
* stable release
1.0.0-pre5 / 2014-05-22
=======================
* package: bump `socket.io-client` for parser fixes
* package: bump `engine.io`
1.0.0-pre4 / 2014-05-19
=======================
* package: bump client
1.0.0-pre3 / 2014-05-17
=======================
* package: bump parser
* package: bump engine.io
1.0.0-pre2 / 2014-04-27
=======================
* package: bump `engine.io`
* added backwards compatible of engine.io maxHttpBufferSize
* added test that server and client using same protocol
* added support for setting allowed origins
* added information about logging
* the set function in server can be used to set some attributes for BC
* fix error in callback call 'done' instead of 'next' in docs
* package: bump `socket.io-parser`
* package: bump `expect.js`
* added some new tests, including binary with acks
1.0.0-pre / 2014-03-14
======================
* implemented `engine.io`
* implemented `socket.io-adapter`
* implemented `socket.io-protocol`
* implemented `debug` and improved instrumentation
* added binary support
* added new `require('io')(srv)` signature
* simplified `socket.io-client` serving
* transports: added escaping to htmlfile (fixes #1251)
0.9.14 / 2013-03-29
===================

View File

@@ -1,6 +1,6 @@
(The MIT License)
Copyright (c) 2014 Automattic <dev@cloudup.com>
Copyright (c) 2011 Guillermo Rauch <guillermo@learnboost.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the

View File

@@ -1,15 +1,31 @@
REPORTER = dot
ALL_TESTS = $(shell find test/ -name '*.test.js')
ALL_BENCH = $(shell find benchmarks -name '*.bench.js')
run-tests:
@./node_modules/.bin/expresso \
-t 3000 \
-I support \
--serial \
$(TESTFLAGS) \
$(TESTS)
test:
@./node_modules/.bin/mocha \
--reporter $(REPORTER) \
--slow 200ms \
--bail
@$(MAKE) NODE_PATH=lib TESTS="$(ALL_TESTS)" run-tests
test-cov:
@./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- \
--reporter $(REPORTER) \
test/
@TESTFLAGS=--cov $(MAKE) test
.PHONY: test
test-leaks:
@ls test/leaks/* | xargs node --expose_debug_as=debug --expose_gc
run-bench:
@node $(PROFILEFLAGS) benchmarks/runner.js
bench:
@$(MAKE) BENCHMARKS="$(ALL_BENCH)" run-bench
profile:
@PROFILEFLAGS='--prof --trace-opt --trace-bailout --trace-deopt' $(MAKE) bench
.PHONY: test bench profile

651
Readme.md
View File

@@ -1,349 +1,364 @@
### This Readme corresponds to the upcoming 1.0 release. Please refer to http://socket.io for the current 0.9.x documentation.
# Socket.IO
<hr />
Socket.IO is a Node.JS project that makes WebSockets and realtime possible in
all browsers. It also enhances WebSockets by providing built-in multiplexing,
horizontal scalability, automatic JSON encoding/decoding, and more.
# socket.io
## How to Install
[![Build Status](https://secure.travis-ci.org/LearnBoost/socket.io.png)](http://travis-ci.org/LearnBoost/socket.io)
[![NPM version](https://badge.fury.io/js/socket.io.png)](http://badge.fury.io/js/socket.io)
```bash
npm install socket.io
```
## How to use
The following example attaches socket.io to a plain Node.JS
HTTP server listening on port `3000`.
First, require `socket.io`:
```js
var server = require('http').Server();
var io = require('socket.io')(server);
io.on('connection', function(socket){
socket.on('event', function(data){});
socket.on('disconnect', function(){});
var io = require('socket.io');
```
Next, attach it to a HTTP/HTTPS server. If you're using the fantastic `express`
web framework:
#### Express 3.x
```js
var app = express()
, server = require('http').createServer(app)
, io = io.listen(server);
server.listen(80);
io.sockets.on('connection', function (socket) {
socket.emit('news', { hello: 'world' });
socket.on('my other event', function (data) {
console.log(data);
});
});
server.listen(3000);
```
### Standalone
#### Express 2.x
```js
var io = require('socket.io')();
io.on('connection', function(socket){});
io.listen(3000);
var app = express.createServer()
, io = io.listen(app);
app.listen(80);
io.sockets.on('connection', function (socket) {
socket.emit('news', { hello: 'world' });
socket.on('my other event', function (data) {
console.log(data);
});
});
```
### In conjunction with Express
Finally, load it from the client side code:
Starting with **3.0**, express applications have become request handler
functions that you pass to `http` or `http` `Server` instances. You need
to pass the `Server` to `socket.io`, and not the express application
function.
```html
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io.connect('http://localhost');
socket.on('news', function (data) {
console.log(data);
socket.emit('my other event', { my: 'data' });
});
</script>
```
For more thorough examples, look at the `examples/` directory.
## Short recipes
### Sending and receiving events.
Socket.IO allows you to emit and receive custom events.
Besides `connect`, `message` and `disconnect`, you can emit custom events:
```js
var app = require('express')();
var server = require('http').Server(app);
var io = require('socket.io')(server);
io.on('connection', function(){ /* … */ });
server.listen(3000);
// note, io.listen(<port>) will create a http server for you
var io = require('socket.io').listen(80);
io.sockets.on('connection', function (socket) {
io.sockets.emit('this', { will: 'be received by everyone' });
socket.on('private message', function (from, msg) {
console.log('I received a private message by ', from, ' saying ', msg);
});
socket.on('disconnect', function () {
io.sockets.emit('user disconnected');
});
});
```
### In conjunction with Koa
### Storing data associated to a client
Like Express.JS, Koa works by exposing an application as a request
handler function, but only by calling the `callback` method.
Sometimes it's necessary to store data associated with a client that's
necessary for the duration of the session.
#### Server side
```js
var app = require('koa')();
var server = require('http').Server(app.callback());
var io = require('socket.io')(server);
io.on('connection', function(){ /* … */ });
server.listen(3000);
var io = require('socket.io').listen(80);
io.sockets.on('connection', function (socket) {
socket.on('set nickname', function (name) {
socket.set('nickname', name, function () { socket.emit('ready'); });
});
socket.on('msg', function () {
socket.get('nickname', function (err, name) {
console.log('Chat message by ', name);
});
});
});
```
## API
#### Client side
### Server
```html
<script>
var socket = io.connect('http://localhost');
Exposed by `require('socket.io')`.
### Server()
Creates a new `Server`. Works with and without `new`:
```js
var io = require('socket.io')();
// or
var Server = require('socket.io');
var io = new Server();
```
### Server(opts:Object)
Optionally, the first or second argument (see below) of the `Server`
constructor can be an options object.
The following options are supported:
- `serveClient` sets the value for Server#serveClient()
- `path` sets the value for Server#path()
The same options passed to socket.io are always passed to
the `engine.io` `Server` that gets created. See engine.io
[options](https://github.com/learnboost/engine.io#methods-1)
as reference.
### Server(srv:http#Server, opts:Object)
Creates a new `Server` and attaches it to the given `srv`. Optionally
`opts` can be passed.
### Server(port:Number, opts:Object)
Binds socket.io to a new `http.Server` that listens on `port`.
### Server#serveClient(v:Boolean):Server
If `v` is `true` the attached server (see `Server#attach`) will serve
the client files. Defaults to `true`.
This method has no effect after `attach` is called.
```js
// pass a server and the `serveClient` option
var io = require('socket.io')(http, { serveClient: false });
// or pass no server and then you can call the method
var io = require('socket.io')();
io.serveClient(false);
io.attach(http);
```
If no arguments are supplied this method returns the current value.
### Server#path(v:String):Server
Sets the path `v` under which `engine.io` and the static files will be
served. Defaults to `/socket.io`.
If no arguments are supplied this method returns the current value.
### Server#adapter(v:Adapter):Server
Sets the adapter `v`. Defaults to an instance of the `Adapter` that
ships with socket.io which is memory based. See
[socket.io-adapter](https://github.com/learnboost/socket.io-adapter).
If no arguments are supplied this method returns the current value.
### Server#origins(v:String):Server
Sets the allowed origins `v`. Defaults to any origins being allowed.
If no arguments are supplied this method returns the current value.
### Server#sockets:Namespace
The default (`/`) namespace.
### Server#attach(srv:http#Server, opts:Object):Server
Attaches the `Server` to an engine.io instance on `srv` with the
supplied `opts` (optionally).
### Server#attach(port:Number, opts:Object):Server
Attaches the `Server` to an engine.io instance that is bound to `port`
with the given `opts` (optionally).
### Server#listen
Synonym of `Server#attach`.
### Server#bind(srv:engine#Server):Server
Advanced use only. Binds the server to a specific engine.io `Server`
(or compatible API) instance.
### Server#onconnection(socket:engine#Socket):Server
Advanced use only. Creates a new `socket.io` client from the incoming
engine.io (or compatible API) `socket`.
### Server#of(nsp:String):Namespace
Initializes and retrieves the given `Namespace` by its pathname
identifier `nsp`.
If the namespace was already initialized it returns it right away.
### Server#emit
Emits an event to all connected clients. The following two are
equivalent:
```js
var io = require('socket.io')();
io.sockets.emit('an event sent to all connected clients');
io.emit('an event sent to all connected clients');
```
For other available methods, see `Namespace` below.
### Server#use
See `Namespace#use` below.
### Namespace
Represents a pool of sockets connected under a given scope identified
by a pathname (eg: `/chat`).
By default the client always connects to `/`.
#### Events
- `connection` / `connect`. Fired upon a connection.
Parameters:
- `Socket` the incoming socket.
### Namespace#name:String
The namespace identifier property.
### Namespace#connected:Object<Socket>
Hash of `Socket` objects that are connected to this namespace indexed
by `id`.
### Namespace#use(fn:Function):Namespace
Registers a middleware, which is a function that gets executed for
every incoming `Socket` and receives as parameter the socket and a
function to optionally defer execution to the next registered
middleware.
```js
var io = require('socket.io')();
io.use(function(socket, next){
if (socket.request.headers.cookie) return next();
next(new Error('Authentication error'));
socket.on('connect', function () {
socket.emit('set nickname', prompt('What is your nickname?'));
socket.on('ready', function () {
console.log('Connected !');
socket.emit('msg', prompt('What is your message?'));
});
});
```
Errors passed to middleware callbacks are sent as special `error`
packets to clients.
### Socket
A `Socket` is the fundamental class for interacting with browser
clients. A `Socket` belongs to a certain `Namespace` (by default `/`)
and uses an underlying `Client` to communicate.
### Socket#rooms:Array
A list of strings identifying the rooms this socket is in.
### Socket#client:Client
A reference to the underlying `Client` object.
### Socket#conn:Socket
A reference to the underyling `Client` transport connection (engine.io
`Socket` object).
### Socket#request:Request
A getter proxy that returns the reference to the `request` that
originated the underlying engine.io `Client`. Useful for accessing
request headers such as `Cookie` or `User-Agent`.
### Socket#id:String
A unique identifier for the socket session, that comes from the
underlying `Client`.
### Socket#emit(name:String[, …]):Socket
Emits an event to the socket identified by the string `name`. Any
other parameters can be included.
All datastructures are supported, including `Buffer`. JavaScript
functions can't be serialized/deserialized.
```js
var io = require('socket.io')();
io.on('connection', function(socket){
socket.emit('an event', { some: 'data' });
});
```
### Socket#join(name:String[, fn:Function]):Socket
Adds the socket to the `room`, and fires optionally a callback `fn`
with `err` signature (if any).
The socket is automatically a member of a room identified with its
session id (see `Socket#id`).
The mechanics of joining rooms are handled by the `Adapter`
that has been configured (see `Server#adapter` above), defaulting to
[socket.io-adapter](https://github.com/socket.io/socket.io-adapter).
### Socket#leave(name:String[, fn:Function]):Socket
Removes the socket from `room`, and fires optionally a callback `fn`
with `err` signature (if any).
**Rooms are left automatically upon disconnection**.
The mechanics of leaving rooms are handled by the `Adapter`
that has been configured (see `Server#adapter` above), defaulting to
[socket.io-adapter](https://github.com/socket.io/socket.io-adapter).
### Socket#to(room:String):Socket
### Socket#in(room:String):Socket
Sets a modifier for a subsequent event emission that the event will
only be _broadcasted_ to sockets that have joined the given `room`.
To emit to multiple rooms, you can call `to` several times.
```js
var io = require('socket.io')();
io.on('connection', function(socket){
socket.to('others').emit('an event', { some: 'data' });
});
```
### Client
The `Client` class represents an incoming transport (engine.io)
connection. A `Client` can be associated with many multiplexed `Socket`
that belong to different `Namespace`s.
### Client#conn
A reference to the underlying `engine.io` `Socket` connection.
### Client#request
A getter proxy that returns the reference to the `request` that
originated the engine.io connection. Useful for accessing
request headers such as `Cookie` or `User-Agent`.
## Debug / logging
Socket.IO is powered by [debug](http://github.com/visionmedia/debug).
In order to see all the debug output, run your app with the environment variable
`DEBUG` including the desired scope.
To see the output from all of Socket.IO's debugging scopes you can use:
```
DEBUG=socket.io* node myapp
</script>
```
## License
### Restricting yourself to a namespace
MIT
If you have control over all the messages and events emitted for a particular
application, using the default `/` namespace works.
If you want to leverage 3rd-party code, or produce code to share with others,
socket.io provides a way of namespacing a `socket`.
This has the benefit of `multiplexing` a single connection. Instead of
socket.io using two `WebSocket` connections, it'll use one.
The following example defines a socket that listens on '/chat' and one for
'/news':
#### Server side
```js
var io = require('socket.io').listen(80);
var chat = io
.of('/chat')
.on('connection', function (socket) {
socket.emit('a message', { that: 'only', '/chat': 'will get' });
chat.emit('a message', { everyone: 'in', '/chat': 'will get' });
});
var news = io
.of('/news');
.on('connection', function (socket) {
socket.emit('item', { news: 'item' });
});
```
#### Client side:
```html
<script>
var chat = io.connect('http://localhost/chat')
, news = io.connect('http://localhost/news');
chat.on('connect', function () {
chat.emit('hi!');
});
news.on('news', function () {
news.emit('woot');
});
</script>
```
### Sending volatile messages.
Sometimes certain messages can be dropped. Let's say you have an app that
shows realtime tweets for the keyword `bieber`.
If a certain client is not ready to receive messages (because of network slowness
or other issues, or because he's connected through long polling and is in the
middle of a request-response cycle), if he doesn't receive ALL the tweets related
to bieber your application won't suffer.
In that case, you might want to send those messages as volatile messages.
#### Server side
```js
var io = require('socket.io').listen(80);
io.sockets.on('connection', function (socket) {
var tweets = setInterval(function () {
getBieberTweet(function (tweet) {
socket.volatile.emit('bieber tweet', tweet);
});
}, 100);
socket.on('disconnect', function () {
clearInterval(tweets);
});
});
```
#### Client side
In the client side, messages are received the same way whether they're volatile
or not.
### Getting acknowledgements
Sometimes, you might want to get a callback when the client confirmed the message
reception.
To do this, simply pass a function as the last parameter of `.send` or `.emit`.
What's more, when you use `.emit`, the acknowledgement is done by you, which
means you can also pass data along:
#### Server side
```js
var io = require('socket.io').listen(80);
io.sockets.on('connection', function (socket) {
socket.on('ferret', function (name, fn) {
fn('woot');
});
});
```
#### Client side
```html
<script>
var socket = io.connect(); // TIP: .connect with no args does auto-discovery
socket.on('connect', function () { // TIP: you can avoid listening on `connect` and listen on events directly too!
socket.emit('ferret', 'tobi', function (data) {
console.log(data); // data will be 'woot'
});
});
</script>
```
### Broadcasting messages
To broadcast, simply add a `broadcast` flag to `emit` and `send` method calls.
Broadcasting means sending a message to everyone else except for the socket
that starts it.
#### Server side
```js
var io = require('socket.io').listen(80);
io.sockets.on('connection', function (socket) {
socket.broadcast.emit('user connected');
socket.broadcast.json.send({ a: 'message' });
});
```
### Rooms
Sometimes you want to put certain sockets in the same room, so that it's easy
to broadcast to all of them together.
Think of this as built-in channels for sockets. Sockets `join` and `leave`
rooms in each socket.
#### Server side
```js
var io = require('socket.io').listen(80);
io.sockets.on('connection', function (socket) {
socket.join('justin bieber fans');
socket.broadcast.to('justin bieber fans').emit('new fan');
io.sockets.in('rammstein fans').emit('new non-fan');
});
```
### Using it just as a cross-browser WebSocket
If you just want the WebSocket semantics, you can do that too.
Simply leverage `send` and listen on the `message` event:
#### Server side
```js
var io = require('socket.io').listen(80);
io.sockets.on('connection', function (socket) {
socket.on('message', function () { });
socket.on('disconnect', function () { });
});
```
#### Client side
```html
<script>
var socket = io.connect('http://localhost/');
socket.on('connect', function () {
socket.send('hi');
socket.on('message', function (msg) {
// my msg
});
});
</script>
```
### Changing configuration
Configuration in socket.io is TJ-style:
#### Server side
```js
var io = require('socket.io').listen(80);
io.configure(function () {
io.set('transports', ['websocket', 'flashsocket', 'xhr-polling']);
});
io.configure('development', function () {
io.set('transports', ['websocket', 'xhr-polling']);
io.enable('log');
});
```
## License
(The MIT License)
Copyright (c) 2011 Guillermo Rauch &lt;guillermo@learnboost.com&gt;
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 the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,64 @@
/**
* Module dependencies.
*/
var benchmark = require('benchmark')
, colors = require('colors')
, io = require('../')
, parser = io.parser
, suite = new benchmark.Suite('Decode packet');
suite.add('string', function () {
parser.decodePacket('4:::"2"');
});
suite.add('event', function () {
parser.decodePacket('5:::{"name":"woot"}');
});
suite.add('event+ack', function () {
parser.decodePacket('5:1+::{"name":"tobi"}');
});
suite.add('event+data', function () {
parser.decodePacket('5:::{"name":"edwald","args":[{"a": "b"},2,"3"]}');
});
suite.add('heartbeat', function () {
parser.decodePacket('2:::');
});
suite.add('error', function () {
parser.decodePacket('7:::2+0');
});
var payload = parser.encodePayload([
parser.encodePacket({ type: 'message', data: '5', endpoint: '' })
, parser.encodePacket({ type: 'message', data: '53d', endpoint: '' })
, parser.encodePacket({ type: 'message', data: 'foobar', endpoint: '' })
, parser.encodePacket({ type: 'message', data: 'foobarbaz', endpoint: '' })
, parser.encodePacket({ type: 'message', data: 'foobarbazfoobarbaz', endpoint: '' })
, parser.encodePacket({ type: 'message', data: 'foobarbaz', endpoint: '' })
, parser.encodePacket({ type: 'message', data: 'foobar', endpoint: '' })
]);
suite.add('payload', function () {
parser.decodePayload(payload);
});
suite.on('cycle', function (bench, details) {
console.log('\n' + suite.name.grey, details.name.white.bold);
console.log([
details.hz.toFixed(2).cyan + ' ops/sec'.grey
, details.count.toString().white + ' times executed'.grey
, 'benchmark took '.grey + details.times.elapsed.toString().white + ' sec.'.grey
,
].join(', '.grey));
});
if (!module.parent) {
suite.run();
} else {
module.exports = suite;
}

View File

@@ -0,0 +1,90 @@
/**
* Module dependencies.
*/
var benchmark = require('benchmark')
, colors = require('colors')
, io = require('../')
, parser = io.parser
, suite = new benchmark.Suite('Encode packet');
suite.add('string', function () {
parser.encodePacket({
type: 'json'
, endpoint: ''
, data: '2'
});
});
suite.add('event', function () {
parser.encodePacket({
type: 'event'
, name: 'woot'
, endpoint: ''
, args: []
});
});
suite.add('event+ack', function () {
parser.encodePacket({
type: 'json'
, id: 1
, ack: 'data'
, endpoint: ''
, data: { a: 'b' }
});
});
suite.add('event+data', function () {
parser.encodePacket({
type: 'event'
, name: 'edwald'
, endpoint: ''
, args: [{a: 'b'}, 2, '3']
});
});
suite.add('heartbeat', function () {
parser.encodePacket({
type: 'heartbeat'
, endpoint: ''
})
});
suite.add('error', function () {
parser.encodePacket({
type: 'error'
, reason: 'unauthorized'
, advice: 'reconnect'
, endpoint: ''
})
})
suite.add('payload', function () {
parser.encodePayload([
parser.encodePacket({ type: 'message', data: '5', endpoint: '' })
, parser.encodePacket({ type: 'message', data: '53d', endpoint: '' })
, parser.encodePacket({ type: 'message', data: 'foobar', endpoint: '' })
, parser.encodePacket({ type: 'message', data: 'foobarbaz', endpoint: '' })
, parser.encodePacket({ type: 'message', data: 'foobarbazfoobarbaz', endpoint: '' })
, parser.encodePacket({ type: 'message', data: 'foobarbaz', endpoint: '' })
, parser.encodePacket({ type: 'message', data: 'foobar', endpoint: '' })
]);
});
suite.on('cycle', function (bench, details) {
console.log('\n' + suite.name.grey, details.name.white.bold);
console.log([
details.hz.toFixed(2).cyan + ' ops/sec'.grey
, details.count.toString().white + ' times executed'.grey
, 'benchmark took '.grey + details.times.elapsed.toString().white + ' sec.'.grey
,
].join(', '.grey));
});
if (!module.parent) {
suite.run();
} else {
module.exports = suite;
}

55
benchmarks/runner.js Normal file
View File

@@ -0,0 +1,55 @@
/**
* Benchmark runner dependencies
*/
var colors = require('colors')
, path = require('path');
/**
* Find all the benchmarks
*/
var benchmarks_files = process.env.BENCHMARKS.split(' ')
, all = [].concat(benchmarks_files)
, first = all.shift()
, benchmarks = {};
// find the benchmarks and load them all in our obj
benchmarks_files.forEach(function (file) {
benchmarks[file] = require(path.join(__dirname, '..', file));
});
// setup the complete listeners
benchmarks_files.forEach(function (file) {
var benchmark = benchmarks[file]
, next_file = all.shift()
, next = benchmarks[next_file];
/**
* Generate a oncomplete function for the tests, either we are done or we
* have more benchmarks to process.
*/
function complete () {
if (!next) {
console.log(
'\n\nBenchmark completed in'.grey
, (Date.now() - start).toString().green + ' ms'.grey
);
} else {
console.log('\nStarting benchmark '.grey + next_file.yellow);
next.run();
}
}
// attach the listener
benchmark.on('complete', complete);
});
/**
* Start the benchmark
*/
var start = Date.now();
console.log('Starting benchmark '.grey + first.yellow);
benchmarks[first].run();

View File

@@ -1,22 +0,0 @@
# Socket.IO Chat
A simple chat demo for socket.io
## How to use
```
$ npm install
$ node .
```
And point your browser to `http://localhost:3000`. Optionally specify
a port by supplying the `PORT` env variable.
## Features
- Multiple users can join a chat room by entering a unique username
on website load.
- Users can type chat messages to the chat room
- A notification is sent to all users when a user joins or leaves
the chatroom

80
examples/chat/app.js Normal file
View File

@@ -0,0 +1,80 @@
/**
* Module dependencies.
*/
var express = require('express')
, stylus = require('stylus')
, nib = require('nib')
, sio = require('../../lib/socket.io');
/**
* App.
*/
var app = express.createServer();
/**
* App configuration.
*/
app.configure(function () {
app.use(stylus.middleware({ src: __dirname + '/public', compile: compile }));
app.use(express.static(__dirname + '/public'));
app.set('views', __dirname);
app.set('view engine', 'jade');
function compile (str, path) {
return stylus(str)
.set('filename', path)
.use(nib());
};
});
/**
* App routes.
*/
app.get('/', function (req, res) {
res.render('index', { layout: false });
});
/**
* App listen.
*/
app.listen(3000, function () {
var addr = app.address();
console.log(' app listening on http://' + addr.address + ':' + addr.port);
});
/**
* Socket.IO server (single process only)
*/
var io = sio.listen(app)
, nicknames = {};
io.sockets.on('connection', function (socket) {
socket.on('user message', function (msg) {
socket.broadcast.emit('user message', socket.nickname, msg);
});
socket.on('nickname', function (nick, fn) {
if (nicknames[nick]) {
fn(true);
} else {
fn(false);
nicknames[nick] = socket.nickname = nick;
socket.broadcast.emit('announcement', nick + ' connected');
io.sockets.emit('nicknames', nicknames);
}
});
socket.on('disconnect', function () {
if (!socket.nickname) return;
delete nicknames[socket.nickname];
socket.broadcast.emit('announcement', socket.nickname + ' disconnected');
socket.broadcast.emit('nicknames', nicknames);
});
});

83
examples/chat/index.jade Normal file
View File

@@ -0,0 +1,83 @@
doctype 5
html
head
link(href='/stylesheets/style.css', rel='stylesheet')
script(src='http://code.jquery.com/jquery-1.6.1.min.js')
script(src='/socket.io/socket.io.js')
script
// socket.io specific code
var socket = io.connect();
socket.on('connect', function () {
$('#chat').addClass('connected');
});
socket.on('announcement', function (msg) {
$('#lines').append($('<p>').append($('<em>').text(msg)));
});
socket.on('nicknames', function (nicknames) {
$('#nicknames').empty().append($('<span>Online: </span>'));
for (var i in nicknames) {
$('#nicknames').append($('<b>').text(nicknames[i]));
}
});
socket.on('user message', message);
socket.on('reconnect', function () {
$('#lines').remove();
message('System', 'Reconnected to the server');
});
socket.on('reconnecting', function () {
message('System', 'Attempting to re-connect to the server');
});
socket.on('error', function (e) {
message('System', e ? e : 'A unknown error occurred');
});
function message (from, msg) {
$('#lines').append($('<p>').append($('<b>').text(from), msg));
}
// dom manipulation
$(function () {
$('#set-nickname').submit(function (ev) {
socket.emit('nickname', $('#nick').val(), function (set) {
if (!set) {
clear();
return $('#chat').addClass('nickname-set');
}
$('#nickname-err').css('visibility', 'visible');
});
return false;
});
$('#send-message').submit(function () {
message('me', $('#message').val());
socket.emit('user message', $('#message').val());
clear();
$('#lines').get(0).scrollTop = 10000000;
return false;
});
function clear () {
$('#message').val('').focus();
};
});
body
#chat
#nickname
form.wrap#set-nickname
p Please type in your nickname and press enter.
input#nick
p#nickname-err Nickname already in use
#connecting
.wrap Connecting to socket.io server
#messages
#nicknames
#lines
form#send-message
input#message
button Send

View File

@@ -1,79 +0,0 @@
// Setup basic express server
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var io = require('../..')(server);
var port = process.env.PORT || 3000;
server.listen(port, function () {
console.log('Server listening at port %d', port);
});
// Routing
app.use(express.static(__dirname + '/public'));
// Chatroom
// usernames which are currently connected to the chat
var usernames = {};
var numUsers = 0;
io.on('connection', function (socket) {
var addedUser = false;
// when the client emits 'new message', this listens and executes
socket.on('new message', function (data) {
// we tell the client to execute 'new message'
socket.broadcast.emit('new message', {
username: socket.username,
message: data
});
});
// when the client emits 'add user', this listens and executes
socket.on('add user', function (username) {
// we store the username in the socket session for this client
socket.username = username;
// add the client's username to the global list
usernames[username] = username;
++numUsers;
addedUser = true;
socket.emit('login', {
numUsers: numUsers
});
// echo globally (all clients) that a person has connected
socket.broadcast.emit('user joined', {
username: socket.username,
numUsers: numUsers
});
});
// when the client emits 'typing', we broadcast it to others
socket.on('typing', function () {
socket.broadcast.emit('typing', {
username: socket.username
});
});
// when the client emits 'stop typing', we broadcast it to others
socket.on('stop typing', function () {
socket.broadcast.emit('stop typing', {
username: socket.username
});
});
// when the user disconnects.. perform this
socket.on('disconnect', function () {
// remove the username from global usernames list
if (addedUser) {
delete usernames[socket.username];
--numUsers;
// echo globally that this client has left
socket.broadcast.emit('user left', {
username: socket.username,
numUsers: numUsers
});
}
});
});

View File

@@ -1,12 +1,11 @@
{
"name": "socket.io-chat",
"version": "0.0.0",
"description": "A simple chat client using socket.io",
"main": "app.js",
"author": "Grant Timmerman",
"private": true,
"license": "BSD",
"dependencies": {
"express": "3.4.8"
}
}
"name": "chat.io"
, "description": "example chat application with socket.io"
, "version": "0.0.1"
, "dependencies": {
"express": "2.5.5"
, "jade": "0.16.4"
, "stylus": "0.19.0"
, "nib": "0.2.0"
}
}

View File

@@ -1,28 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Socket.IO Chat Example</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<ul class="pages">
<li class="chat page">
<div class="chatArea">
<ul class="messages"></ul>
</div>
<input class="inputMessage" placeholder="Type here..."/>
</li>
<li class="login page">
<div class="form">
<h3 class="title">What's your nickname?</h3>
<input class="usernameInput" type="text" maxlength="14" />
</div>
</li>
</ul>
<script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="/main.js"></script>
</body>
</html>

View File

@@ -1,265 +0,0 @@
$(function() {
var FADE_TIME = 150; // ms
var TYPING_TIMER_LENGTH = 400; // ms
var COLORS = [
'#e21400', '#91580f', '#f8a700', '#f78b00',
'#58dc00', '#287b00', '#a8f07a', '#4ae8c4',
'#3b88eb', '#3824aa', '#a700ff', '#d300e7'
];
// Initialize varibles
var $window = $(window);
var $usernameInput = $('.usernameInput'); // Input for username
var $messages = $('.messages'); // Messages area
var $inputMessage = $('.inputMessage'); // Input message input box
var $loginPage = $('.login.page'); // The login page
var $chatPage = $('.chat.page'); // The chatroom page
// Prompt for setting a username
var username;
var connected = false;
var typing = false;
var lastTypingTime;
var $currentInput = $usernameInput.focus();
var socket = io();
function addParticipantsMessage (data) {
var message = '';
if (data.numUsers === 1) {
message += "there's 1 participant";
} else {
message += "there're " + data.numUsers + " participants";
}
log(message);
}
// Sets the client's username
function setUsername () {
username = cleanInput($usernameInput.val().trim());
// If the username is valid
if (username) {
$loginPage.fadeOut();
$chatPage.show();
$loginPage.off('click');
$currentInput = $inputMessage.focus();
// Tell the server your username
socket.emit('add user', username);
}
}
// Sends a chat message
function sendMessage () {
var message = $inputMessage.val();
// Prevent markup from being injected into the message
message = cleanInput(message);
// if there is a non-empty message and a socket connection
if (message && connected) {
$inputMessage.val('');
addChatMessage({
username: username,
message: message
});
// tell server to execute 'new message' and send along one parameter
socket.emit('new message', message);
}
}
// Log a message
function log (message, options) {
var el = '<li class="log">' + message + '</li>';
addMessageElement(el, options);
}
// Adds the visual chat message to the message list
function addChatMessage (data, options) {
// Don't fade the message in if there is an 'X was typing'
var $typingMessages = getTypingMessages(data);
options = options || {};
if ($typingMessages.length !== 0) {
options.fade = false;
$typingMessages.remove();
}
var colorStyle = 'style="color:' + getUsernameColor(data.username) + '"';
var usernameDiv = '<span class="username"' + colorStyle + '>' +
data.username + '</span>';
var messageBodyDiv = '<span class="messageBody">' +
data.message + '</span>';
var typingClass = data.typing ? 'typing' : '';
var messageDiv = '<li class="message ' + typingClass + '">' +
usernameDiv + messageBodyDiv + '</li>';
var $messageDiv = $(messageDiv).data('username', data.username);
addMessageElement($messageDiv, options);
}
// Adds the visual chat typing message
function addChatTyping (data) {
data.typing = true;
data.message = 'is typing';
addChatMessage(data);
}
// Removes the visual chat typing message
function removeChatTyping (data) {
getTypingMessages(data).fadeOut(function () {
$(this).remove();
});
}
// Adds a message element to the messages and scrolls to the bottom
// el - The element to add as a message
// options.fade - If the element should fade-in (default = true)
// options.prepend - If the element should prepend
// all other messages (default = false)
function addMessageElement (el, options) {
var $el = $(el);
// Setup default options
if (!options) {
options = {};
}
if (typeof options.fade === 'undefined') {
options.fade = true;
}
if (typeof options.prepend === 'undefined') {
options.prepend = false;
}
// Apply options
if (options.fade) {
$el.hide().fadeIn(FADE_TIME);
}
if (options.prepend) {
$messages.prepend($el);
} else {
$messages.append($el);
}
$messages[0].scrollTop = $messages[0].scrollHeight;
}
// Prevents input from having injected markup
function cleanInput (input) {
return $('<div/>').text(input).html() || input;
}
// Updates the typing event
function updateTyping () {
if (connected) {
if (!typing) {
typing = true;
socket.emit('typing');
}
lastTypingTime = (new Date()).getTime();
setTimeout(function () {
var typingTimer = (new Date()).getTime();
var timeDiff = typingTimer - lastTypingTime;
if (timeDiff >= TYPING_TIMER_LENGTH && typing) {
socket.emit('stop typing');
typing = false;
}
}, TYPING_TIMER_LENGTH);
}
}
// Gets the 'X is typing' messages of a user
function getTypingMessages (data) {
return $('.typing.message').filter(function (i) {
return $(this).data('username') === data.username;
});
}
// Gets the color of a username through our hash function
function getUsernameColor (username) {
// Compute hash code
var hash = 7;
for (var i = 0; i < username.length; i++) {
hash = username.charCodeAt(i) + (hash << 5) - hash;
}
// Calculate color
var index = Math.abs(hash % COLORS.length);
return COLORS[index];
}
// Keyboard events
$window.keydown(function (event) {
// Auto-focus the current input when a key is typed
if (!(event.ctrlKey || event.metaKey || event.altKey)) {
$currentInput.focus();
}
// When the client hits ENTER on their keyboard
if (event.which === 13) {
if (username) {
sendMessage();
socket.emit('stop typing');
typing = false;
} else {
setUsername();
}
}
});
$inputMessage.on('input', function() {
updateTyping();
});
// Click events
// Focus input when clicking anywhere on login page
$loginPage.click(function () {
$currentInput.focus();
});
// Focus input when clicking on the message input's border
$inputMessage.click(function () {
$inputMessage.focus();
});
// Socket events
// Whenever the server emits 'login', log the login message
socket.on('login', function (data) {
connected = true;
// Display the welcome message
var message = "Welcome to Socket.IO Chat &mdash; ";
log(message, {
prepend: true
});
addParticipantsMessage(data);
});
// Whenever the server emits 'new message', update the chat body
socket.on('new message', function (data) {
addChatMessage(data);
});
// Whenever the server emits 'user joined', log it in the chat body
socket.on('user joined', function (data) {
log(data.username + ' joined');
addParticipantsMessage(data);
});
// Whenever the server emits 'user left', log it in the chat body
socket.on('user left', function (data) {
log(data.username + ' left');
addParticipantsMessage(data);
removeChatTyping(data);
});
// Whenever the server emits 'typing', show the typing message
socket.on('typing', function (data) {
addChatTyping(data);
});
// Whenever the server emits 'stop typing', kill the typing message
socket.on('stop typing', function (data) {
removeChatTyping(data);
});
});

View File

@@ -1,146 +0,0 @@
/* Fix user-agent */
* {
box-sizing: border-box;
}
html {
font-family:
"HelveticaNeue-Light",
"Helvetica Neue Light",
"Helvetica Neue",
Helvetica,
Arial,
"Lucida Grande",
sans-serif;
font-weight: 300;
-webkit-font-smoothing: antialiased;
}
html, body {
height: 100%;
margin: 0;
padding: 0;
}
ul {
list-style: none;
}
/* Pages */
.pages {
height: 100%;
margin: 0;
padding: 0;
width: 100%;
}
.page {
height: 100%;
position: absolute;
width: 100%;
}
/* Login Page */
.login.page {
background-color: #000;
}
.login.page .form {
height: 100px;
margin-top: -100px;
position: absolute;
text-align: center;
top: 50%;
width: 100%;
}
.login.page .form .usernameInput {
background-color: transparent;
border: none;
border-bottom: 2px solid #fff;
outline: none;
padding-bottom: 15px;
text-align: center;
width: 400px;
}
.login.page .title {
font-size: 200%;
}
.login.page .usernameInput {
font-size: 200%;
letter-spacing: 3px;
}
.login.page .title, .login.page .usernameInput {
color: #fff;
font-weight: 100;
}
/* Chat page */
.chat.page {
display: none;
}
/* Font */
.messages {
font-size: 150%;
}
.inputMessage {
font-size: 100%;
}
.log {
color: gray;
font-size: 70%;
margin: 5px;
text-align: center;
}
/* Messages */
.chatArea {
height: 100%;
padding-bottom: 60px;
}
.messages {
height: 100%;
margin: 0;
overflow-y: scroll;
padding: 10px 20px 10px 20px;
}
.message.typing .messageBody {
color: gray;
}
.username {
float: left;
font-weight: 700;
overflow: hidden;
padding-right: 15px;
text-align: right;
}
/* Input */
.inputMessage {
border: 10px solid #000;
bottom: 0;
height: 60px;
left: 0;
outline: none;
padding-left: 10px;
position: absolute;
right: 0;
width: 100%;
}

View File

@@ -0,0 +1,96 @@
border-radius(n)
-webkit-border-radius n
-moz-border-radius n
border-radius n
// replace str with val
replace(expr, str, val)
expr = clone(expr)
for e, i in expr
if str == e
expr[i] = val
expr
// normalize gradient point (webkit)
grad-point(pos)
if length(pos) == 1
return left pos if pos in (top bottom)
return pos top if pos in (left right)
else if pos[0] in (top bottom)
pos[1] pos[0]
else
pos
// implicit color stop position
pos-in-stops(i, stops)
len = length(stops)
if len - 1 == i
100%
else if i
unit(i / len * 100, '%')
else
0%
// normalize color stops
// - (color pos) -> (pos color)
// - (color) -> (implied-pos color)
normalize-stops(stops)
stops = clone(stops)
for stop, i in stops
if length(stop) == 1
color = stop[0]
stop[0] = pos-in-stops(i, stops)
stop[1] = color
else if typeof(stop[1]) == 'unit'
pos = stop[1]
stop[1] = stop[0]
stop[0] = pos
stops
// join color stops with the given translation function
join-stops(stops, translate)
str = ''
len = length(stops)
for stop, i in stops
str += ', ' if i
pos = stop[0]
color = stop[1]
str += translate(color, pos)
unquote(str)
// webkit translation function
webkit-stop(color, pos)
s('color-stop(%d, %s)', pos / 100, color)
// mozilla translation function
moz-stop(color, pos)
s('%s %s', color, pos)
// create a linear gradient with the given start
// position, followed by color stops
linear-gradient(start, stops...)
error('color stops required') unless length(stops)
prop = current-property[0]
val = current-property[1]
stops = normalize-stops(stops)
// webkit
end = grad-point(opposite-position(start))
webkit = s('-webkit-gradient(linear, %s, %s, %s)', grad-point(start), end, join-stops(stops, webkit-stop))
add-property(prop, replace(val, '__CALL__', webkit))
// moz
stops = join-stops(stops, moz-stop)
moz = s('-moz-linear-gradient(%s, %s)', start, stops)
add-property(prop, replace(val, '__CALL__', moz))
// literal
s('linear-gradient(%s, %s)', start, stops)

View File

@@ -0,0 +1,188 @@
#chat,
#nickname,
#messages {
width: 600px;
}
#chat {
position: relative;
border: 1px solid #ccc;
}
#nickname,
#connecting {
position: absolute;
height: 410px;
z-index: 100;
left: 0;
top: 0;
background: #fff;
text-align: center;
width: 600px;
font: 15px Georgia;
color: #666;
display: block;
}
#nickname .wrap,
#connecting .wrap {
padding-top: 150px;
}
#nickname input {
border: 1px solid #ccc;
padding: 10px;
}
#nickname input:focus {
border-color: #999;
outline: 0;
}
#nickname #nickname-err {
color: #8b0000;
font-size: 12px;
visibility: hidden;
}
.connected #connecting {
display: none;
}
.nickname-set #nickname {
display: none;
}
#messages {
height: 380px;
background: #eee;
}
#messages em {
text-shadow: 0 1px 0 #fff;
color: #999;
}
#messages p {
padding: 0;
margin: 0;
font: 12px Helvetica, Arial;
padding: 5px 10px;
}
#messages p b {
display: inline-block;
padding-right: 10px;
}
#messages p:nth-child(even) {
background: #fafafa;
}
#messages #nicknames {
background: #ccc;
padding: 2px 4px 4px;
font: 11px Helvetica;
}
#messages #nicknames span {
color: #000;
}
#messages #nicknames b {
display: inline-block;
color: #fff;
background: #999;
padding: 3px 6px;
margin-right: 5px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
text-shadow: 0 1px 0 #666;
}
#messages #lines {
height: 355px;
overflow: auto;
overflow-x: hidden;
overflow-y: auto;
}
#messages #lines::-webkit-scrollbar {
width: 6px;
height: 6px;
}
#messages #lines::-webkit-scrollbar-button:start:decrement,
#messages #lines ::-webkit-scrollbar-button:end:increment {
display: block;
height: 10px;
}
#messages #lines::-webkit-scrollbar-button:vertical:increment {
background-color: #fff;
}
#messages #lines::-webkit-scrollbar-track-piece {
background-color: #fff;
-webkit-border-radius: 3px;
}
#messages #lines::-webkit-scrollbar-thumb:vertical {
height: 50px;
background-color: #ccc;
-webkit-border-radius: 3px;
}
#messages #lines::-webkit-scrollbar-thumb:horizontal {
width: 50px;
background-color: #fff;
-webkit-border-radius: 3px;
}
#send-message {
background: #fff;
position: relative;
}
#send-message input {
border: none;
height: 30px;
padding: 0 10px;
line-height: 30px;
vertical-align: middle;
width: 580px;
}
#send-message input:focus {
outline: 0;
}
#send-message button {
position: absolute;
top: 5px;
right: 5px;
}
button {
margin: 0;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
display: inline-block;
text-decoration: none;
background: #43a1f7;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #43a1f7), color-stop(1, #377ad0));
background: -webkit-linear-gradient(top, #43a1f7 0%, #377ad0 100%);
background: -moz-linear-gradient(top, #43a1f7 0%, #377ad0 100%);
background: linear-gradient(top, #43a1f7 0%, #377ad0 100%);
border: 1px solid #2e70c4;
-webkit-border-radius: 16px;
-moz-border-radius: 16px;
border-radius: 16px;
color: #fff;
font-family: "lucida grande", sans-serif;
font-size: 11px;
font-weight: normal;
line-height: 1;
padding: 3px 10px 5px 10px;
text-align: center;
text-shadow: 0 -1px 1px #2d6dc0;
}
button:hover,
button.hover {
background: darker;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #43a1f7), color-stop(1, #2e70c4));
background: -webkit-linear-gradient(top, #43a1f7 0%, #2e70c4 100%);
background: -moz-linear-gradient(top, #43a1f7 0%, #2e70c4 100%);
background: linear-gradient(top, #43a1f7 0%, #2e70c4 100%);
border: 1px solid #2e70c4;
cursor: pointer;
text-shadow: 0 -1px 1px #2c6bbb;
}
button:active,
button.active {
background: #2e70c4;
border: 1px solid #2e70c4;
border-bottom: 1px solid #2861aa;
text-shadow: 0 -1px 1px #2b67b5;
}
button:focus,
button.focus {
outline: none;
-webkit-box-shadow: 0 1px 0 0 rgba(255,255,255,0.4), 0 0 4px 0 #377ad0;
-moz-box-shadow: 0 1px 0 0 rgba(255,255,255,0.4), 0 0 4px 0 #377ad0;
box-shadow: 0 1px 0 0 rgba(255,255,255,0.4), 0 0 4px 0 #377ad0;
}

View File

@@ -0,0 +1,118 @@
@import 'nib'
#chat, #nickname, #messages
width 600px
#chat
position relative
border 1px solid #ccc
#nickname, #connecting
position absolute
height 410px
z-index 100
left 0
top 0
background #fff
text-align center
width 600px
font 15px Georgia
color #666
display block
.wrap
padding-top 150px
#nickname
input
border 1px solid #ccc
padding 10px
&:focus
border-color #999
outline 0
#nickname-err
color darkred
font-size 12px
visibility hidden
.connected
#connecting
display none
.nickname-set
#nickname
display none
#messages
height 380px
background #eee
em
text-shadow 0 1px 0 #fff
color #999
p
padding 0
margin 0
font 12px Helvetica, Arial
padding 5px 10px
b
display inline-block
padding-right 10px
p:nth-child(even)
background #fafafa
#nicknames
background #ccc
padding 2px 4px 4px
font 11px Helvetica
span
color black
b
display inline-block
color #fff
background #999
padding 3px 6px
margin-right 5px
border-radius 10px
text-shadow 0 1px 0 #666
#lines
height 355px
overflow auto
overflow-x hidden
overflow-y auto
&::-webkit-scrollbar
width 6px
height 6px
&::-webkit-scrollbar-button:start:decrement, ::-webkit-scrollbar-button:end:increment
display block
height 10px
&::-webkit-scrollbar-button:vertical:increment
background-color #fff
&::-webkit-scrollbar-track-piece
background-color #fff
-webkit-border-radius 3px
&::-webkit-scrollbar-thumb:vertical
height 50px
background-color #ccc
-webkit-border-radius 3px
&::-webkit-scrollbar-thumb:horizontal
width 50px
background-color #fff
-webkit-border-radius 3px
#send-message
background #fff
position relative
input
border none
height 30px
padding 0 10px
line-height 30px
vertical-align middle
width 580px
&:focus
outline 0
button
position absolute
top 5px
right 5px
button
download-button()

View File

@@ -0,0 +1,74 @@
/**
* Module dependencies.
*/
var express = require('express')
, stylus = require('stylus')
, nib = require('nib')
, sio = require('../../lib/socket.io')
, irc = require('./irc');
/**
* App.
*/
var app = express.createServer();
/**
* App configuration.
*/
app.configure(function () {
app.use(stylus.middleware({ src: __dirname + '/public', compile: compile }))
app.use(express.static(__dirname + '/public'));
app.set('views', __dirname);
app.set('view engine', 'jade');
function compile (str, path) {
return stylus(str)
.set('filename', path)
.use(nib());
};
});
/**
* App routes.
*/
app.get('/', function (req, res) {
res.render('index', { layout: false });
});
/**
* App listen.
*/
app.listen(3000, function () {
var addr = app.address();
console.log(' app listening on http://' + addr.address + ':' + addr.port);
});
/**
* Socket.IO server
*/
var io = sio.listen(app)
/**
* Connect to IRC.
*/
var client = new irc.Client('irc.freenode.net', 6667);
client.connect('socketio\\test\\' + String(Math.random()).substr(-3));
client.on('001', function () {
this.send('JOIN', '#node.js');
});
client.on('PART', function (prefix) {
io.sockets.emit('announcement', irc.user(prefix) + ' left the channel');
});
client.on('JOIN', function (prefix) {
io.sockets.emit('announcement', irc.user(prefix) + ' joined the channel');
});
client.on('PRIVMSG', function (prefix, channel, text) {
io.sockets.emit('irc message', irc.user(prefix), text);
});

View File

@@ -0,0 +1,28 @@
doctype 5
html
head
link(href='/stylesheets/style.css', rel='stylesheet')
script(src='http://code.jquery.com/jquery-1.6.1.min.js')
script(src='/socket.io/socket.io.js')
script
var socket = io.connect();
socket.on('connect', function () {
$('#irc').addClass('connected');
});
socket.on('announcement', function (msg) {
$('#messages').append($('<p>').append($('<em>').text(msg)));
$('#messages').get(0).scrollTop = 10000000;
});
socket.on('irc message', function (user, msg) {
$('#messages').append($('<p>').append($('<b>').text(user), msg));
$('#messages').get(0).scrollTop = 10000000;
});
body
h2 Node.JS IRC
#irc
#connecting
.wrap Connecting to socket.io server
#messages

164
examples/irc-output/irc.js Normal file
View File

@@ -0,0 +1,164 @@
/**
* From https://github.com/felixge/nodelog/
*/
var sys = require('util');
var tcp = require('net');
var irc = exports;
function bind(fn, scope) {
var bindArgs = Array.prototype.slice.call(arguments);
bindArgs.shift();
bindArgs.shift();
return function() {
var args = Array.prototype.slice.call(arguments);
fn.apply(scope, bindArgs.concat(args));
};
}
function each(set, iterator) {
for (var i = 0; i < set.length; i++) {
var r = iterator(set[i], i);
if (r === false) {
return;
}
}
}
var Client = irc.Client = function(host, port) {
this.host = host || 'localhost';
this.port = port || 6667;
this.connection = null;
this.buffer = '';
this.encoding = 'utf8';
this.timeout = 10 * 60 * 60 * 1000;
this.nick = null;
this.user = null;
this.real = null;
}
sys.inherits(Client, process.EventEmitter);
Client.prototype.connect = function(nick, user, real) {
var connection = tcp.createConnection(this.port, this.host);
connection.setEncoding(this.encoding);
connection.setTimeout(this.timeout);
connection.addListener('connect', bind(this.onConnect, this));
connection.addListener('data', bind(this.onReceive, this));
connection.addListener('end', bind(this.onEof, this));
connection.addListener('timeout', bind(this.onTimeout, this));
connection.addListener('close', bind(this.onClose, this));
this.nick = nick;
this.user = user || 'guest';
this.real = real || 'Guest';
this.connection = connection;
};
Client.prototype.disconnect = function(why) {
if (this.connection.readyState !== 'closed') {
this.connection.close();
sys.puts('disconnected (reason: '+why+')');
this.emit('DISCONNECT', why);
}
};
Client.prototype.send = function(arg1) {
if (this.connection.readyState !== 'open') {
return this.disconnect('cannot send with readyState: '+this.connection.readyState);
}
var message = [];
for (var i = 0; i< arguments.length; i++) {
if (arguments[i]) {
message.push(arguments[i]);
}
}
message = message.join(' ');
sys.puts('> '+message);
message = message + "\r\n";
this.connection.write(message, this.encoding);
};
Client.prototype.parse = function(message) {
var match = message.match(/(?:(:[^\s]+) )?([^\s]+) (.+)/);
var parsed = {
prefix: match[1],
command: match[2]
};
var params = match[3].match(/(.*?) ?:(.*)/);
if (params) {
// Params before :
params[1] = (params[1])
? params[1].split(' ')
: [];
// Rest after :
params[2] = params[2]
? [params[2]]
: [];
params = params[1].concat(params[2]);
} else {
params = match[3].split(' ');
}
parsed.params = params;
return parsed;
};
Client.prototype.onConnect = function() {
this.send('NICK', this.nick);
this.send('USER', this.user, '0', '*', ':'+this.real);
};
Client.prototype.onReceive = function(chunk) {
this.buffer = this.buffer + chunk;
while (this.buffer) {
var offset = this.buffer.indexOf("\r\n");
if (offset < 0) {
return;
}
var message = this.buffer.substr(0, offset);
this.buffer = this.buffer.substr(offset + 2);
sys.puts('< '+message);
message = this.parse(message);
this.emit.apply(this, [message.command, message.prefix].concat(message.params));
if (message !== false) {
this.onMessage(message);
}
}
};
Client.prototype.onMessage = function(message) {
switch (message.command) {
case 'PING':
this.send('PONG', ':'+message.params[0]);
break;
}
};
Client.prototype.onEof = function() {
this.disconnect('eof');
};
Client.prototype.onTimeout = function() {
this.disconnect('timeout');
};
Client.prototype.onClose = function() {
this.disconnect('close');
};
exports.user = function(prefix) {
return prefix.match(/:([^!]+)!/)[1]
};

View File

@@ -0,0 +1,10 @@
{
"name": "socket.io-irc"
, "version": "0.0.1"
, "dependencies": {
"express": "2.5.5"
, "jade": "0.16.4"
, "stylus": "0.19.0"
, "nib": "0.2.0"
}
}

View File

@@ -0,0 +1,69 @@
@import 'nib'
h2
font bold 18px Helvetica Neue, Arial
#irc, #messages
width 600px
#irc
position relative
border 1px solid #ccc
#connecting
position absolute
height 410px
z-index 100
left 0
top 0
background #fff
text-align center
width 600px
font 15px Georgia
color #666
display block
.wrap
padding-top 150px
.connected
#connecting
display none
#messages
height 380px
background #eee
overflow auto
overflow-x hidden
overflow-y auto
&::-webkit-scrollbar
width 6px
height 6px
&::-webkit-scrollbar-button:start:decrement, ::-webkit-scrollbar-button:end:increment
display block
height 10px
&::-webkit-scrollbar-button:vertical:increment
background-color #fff
&::-webkit-scrollbar-track-piece
background-color #fff
-webkit-border-radius 3px
&::-webkit-scrollbar-thumb:vertical
height 50px
background-color #ccc
-webkit-border-radius 3px
&::-webkit-scrollbar-thumb:horizontal
width 50px
background-color #fff
-webkit-border-radius 3px
em
text-shadow 0 1px 0 #fff
color #999
p
padding 0
margin 0
font 12px Helvetica, Arial
padding 5px 10px
b
display inline-block
padding-right 10px
p:nth-child(even)
background #fafafa

View File

@@ -1,2 +1,8 @@
module.exports = require('./lib');
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
module.exports = require('./lib/socket.io');

View File

@@ -1,219 +0,0 @@
/**
* Module dependencies.
*/
var parser = require('socket.io-parser');
var debug = require('debug')('socket.io:client');
/**
* Module exports.
*/
module.exports = Client;
/**
* Client constructor.
*
* @param {Server} server instance
* @param {Socket} connection
* @api private
*/
function Client(server, conn){
this.server = server;
this.conn = conn;
this.encoder = new parser.Encoder();
this.decoder = new parser.Decoder();
this.id = conn.id;
this.request = conn.request;
this.setup();
this.sockets = [];
this.nsps = {};
this.connectBuffer = [];
}
/**
* Sets up event listeners.
*
* @api private
*/
Client.prototype.setup = function(){
this.onclose = this.onclose.bind(this);
this.ondata = this.ondata.bind(this);
this.ondecoded = this.ondecoded.bind(this);
this.decoder.on('decoded', this.ondecoded);
this.conn.on('data', this.ondata);
this.conn.on('close', this.onclose);
};
/**
* Connects a client to a namespace.
*
* @param {String} namespace name
* @api private
*/
Client.prototype.connect = function(name){
debug('connecting to namespace %s', name);
var nsp = this.server.of(name);
if ('/' != name && !this.nsps['/']) {
this.connectBuffer.push(name);
return;
}
var self = this;
var socket = nsp.add(this, function(){
self.sockets.push(socket);
self.nsps[nsp.name] = socket;
if ('/' == nsp.name) {
self.connectBuffer.forEach(self.connect, self);
delete self.connectBuffer;
}
});
};
/**
* Disconnects from all namespaces and closes transport.
*
* @api private
*/
Client.prototype.disconnect = function(){
var socket;
// we don't use a for loop because the length of
// `sockets` changes upon each iteration
while (socket = this.sockets.shift()) {
socket.disconnect();
}
this.close();
};
/**
* Removes a socket. Called by each `Socket`.
*
* @api private
*/
Client.prototype.remove = function(socket){
var i = this.sockets.indexOf(socket);
if (~i) {
var nsp = this.sockets[i].nsp.name;
this.sockets.splice(i, 1);
delete this.nsps[nsp];
} else {
debug('ignoring remove for %s', socket.id);
}
};
/**
* Closes the underlying connection.
*
* @api private
*/
Client.prototype.close = function(){
if ('open' == this.conn.readyState) {
debug('forcing transport close');
this.conn.close();
this.onclose('forced server close');
}
};
/**
* Writes a packet to the transport.
*
* @param {Object} packet object
* @param {Boolean} whether packet is already encoded
* @param {Boolean} whether packet is volatile
* @api private
*/
Client.prototype.packet = function(packet, preEncoded, volatile){
var self = this;
// this writes to the actual connection
function writeToEngine(encodedPackets) {
if (volatile && !self.conn.transport.writable) return;
for (var i = 0; i < encodedPackets.length; i++) {
self.conn.write(encodedPackets[i]);
}
}
if ('open' == this.conn.readyState) {
debug('writing packet %j', packet);
if(!preEncoded) { // not broadcasting, need to encode
this.encoder.encode(packet, function (encodedPackets) { // encode, then write results to engine
writeToEngine(encodedPackets);
});
} else { // a broadcast pre-encodes a packet
writeToEngine(packet);
}
} else {
debug('ignoring packet write %j', packet);
}
};
/**
* Called with incoming transport data.
*
* @api private
*/
Client.prototype.ondata = function(data){
this.decoder.add(data);
};
/**
* Called when parser fully decodes a packet.
*
* @api private
*/
Client.prototype.ondecoded = function(packet) {
if (parser.CONNECT == packet.type) {
this.connect(packet.nsp);
} else {
var socket = this.nsps[packet.nsp];
if (socket) {
socket.onpacket(packet);
} else {
debug('no socket for namespace %s', packet.nsp);
}
}
};
/**
* Called upon transport close.
*
* @param {String} reason
* @api private
*/
Client.prototype.onclose = function(reason){
debug('client close with reason %s', reason);
// ignore a potential subsequent `close` event
this.destroy();
// `nsps` and `sockets` are cleaned up seamlessly
this.sockets.forEach(function(socket){
socket.onclose(reason);
});
this.decoder.destroy(); // clean up decoder
};
/**
* Cleans up event listeners.
*
* @api private
*/
Client.prototype.destroy = function(){
this.conn.removeListener('data', this.ondata);
this.conn.removeListener('close', this.onclose);
this.decoder.removeListener('decoded', this.ondecoded);
};

View File

@@ -1,351 +0,0 @@
/**
* Module dependencies.
*/
var http = require('http');
var read = require('fs').readFileSync;
var parse = require('url').parse;
var engine = require('engine.io');
var client = require('socket.io-client');
var clientVersion = require('socket.io-client/package').version;
var Client = require('./client');
var Namespace = require('./namespace');
var Adapter = require('socket.io-adapter');
var debug = require('debug')('socket.io:server');
var url = require('url');
/**
* Module exports.
*/
module.exports = Server;
/**
* Socket.IO client source.
*/
var clientSource = read(require.resolve('socket.io-client/socket.io.js'), 'utf-8');
/**
* Server constructor.
*
* @param {http.Server|Number|Object} http server, port or options
* @param {Object} options
* @api public
*/
function Server(srv, opts){
if (!(this instanceof Server)) return new Server(srv, opts);
if ('object' == typeof srv && !srv.listen) {
opts = srv;
srv = null;
}
opts = opts || {};
this.nsps = {};
this.path(opts.path || '/socket.io');
this.serveClient(false !== opts.serveClient);
this.adapter(opts.adapter || Adapter);
this.origins(opts.origins || '*:*');
this.sockets = this.of('/');
if (srv) this.attach(srv, opts);
}
/**
* Server request verification function, that checks for allowed origins
*
* @param {http.IncomingMessage} request
* @param {Function} callback to be called with the result: `fn(err, success)`
*/
Server.prototype.checkRequest = function(req, fn) {
var origin = req.headers.origin || req.headers.referer;
// file:// URLs produce a null Origin which can't be authorized via echo-back
if ('null' == origin) origin = '*';
if (this._origins.indexOf('*:*') !== -1) return fn(null, true);
if (origin) {
try {
var parts = url.parse(origin);
parts.port = parts.port || 80;
var ok =
~this._origins.indexOf(parts.hostname + ':' + parts.port) ||
~this._origins.indexOf(parts.hostname + ':*') ||
~this._origins.indexOf('*:' + parts.port);
return fn(null, !!ok);
} catch (ex) {
}
}
fn(null, false);
};
/**
* Sets/gets whether client code is being served.
*
* @param {Boolean} whether to serve client code
* @return {Server|Boolean} self when setting or value when getting
* @api public
*/
Server.prototype.serveClient = function(v){
if (!arguments.length) return this._serveClient;
this._serveClient = v;
return this;
};
/**
* Old settings for backwards compatibility
*/
var oldSettings = {
"transports": "transports",
"heartbeat timeout": "pingTimeout",
"heartbeat interval": "pingInterval",
"destroy buffer size": "maxHttpBufferSize"
};
/**
* Backwards compatiblity.
*
* @api public
*/
Server.prototype.set = function(key, val){
if ('authorization' == key && val) {
this.use(function(socket, next) {
val(socket.request, function(err, authorized) {
if (err) return next(new Error(err));
if (!authorized) return next(new Error('Not authorized'));
next();
});
});
} else if ('origins' == key && val) {
this.origins(val);
} else if ('resource' == key) {
this.path(val);
} else if (oldSettings[key] && this.eio[oldSettings[key]]) {
this.eio[oldSettings[key]] = val;
} else {
console.error('Option %s is not valid. Please refer to the README.', key);
}
return this;
};
/**
* Sets the client serving path.
*
* @param {String} pathname
* @return {Server|String} self when setting or value when getting
* @api public
*/
Server.prototype.path = function(v){
if (!arguments.length) return this._path;
this._path = v.replace(/\/$/, '');
return this;
};
/**
* Sets the adapter for rooms.
*
* @param {Adapter} pathname
* @return {Server|Adapter} self when setting or value when getting
* @api public
*/
Server.prototype.adapter = function(v){
if (!arguments.length) return this._adapter;
this._adapter = v;
for (var i in this.nsps) {
this.nsps[i].initAdapter();
}
return this;
};
/**
* Sets the allowed origins for requests.
*
* @param {String} origins
* @return {Server|Adapter} self when setting or value when getting
* @api public
*/
Server.prototype.origins = function(v){
if (!arguments.length) return this._origins;
this._origins = v;
return this;
};
/**
* Attaches socket.io to a server or port.
*
* @param {http.Server|Number} server or port
* @param {Object} options passed to engine.io
* @return {Server} self
* @api public
*/
Server.prototype.listen =
Server.prototype.attach = function(srv, opts){
if ('function' == typeof srv) {
var msg = 'You are trying to attach socket.io to an express' +
'request handler function. Please pass a http.Server instance.';
throw new Error(msg);
}
// handle a port as a string
if (Number(srv) == srv) {
srv = Number(srv);
}
if ('number' == typeof srv) {
debug('creating http server and binding to %d', srv);
var port = srv;
srv = http.Server(function(req, res){
res.writeHead(404);
res.end();
});
srv.listen(port);
}
// set engine.io path to `/socket.io`
opts = opts || {};
opts.path = opts.path || '/socket.io';
// set origins verification
opts.allowRequest = this.checkRequest.bind(this);
// initialize engine
debug('creating engine.io instance with opts %j', opts);
this.eio = engine.attach(srv, opts);
// attach static file serving
if (this._serveClient) this.attachServe(srv);
// bind to engine events
this.bind(this.eio);
return this;
};
/**
* Attaches the static file serving.
*
* @param {Function|http.Server} http server
* @api private
*/
Server.prototype.attachServe = function(srv){
debug('attaching client serving req handler');
var url = this._path + '/socket.io.js';
var evs = srv.listeners('request').slice(0);
var self = this;
srv.removeAllListeners('request');
srv.on('request', function(req, res) {
if (0 == req.url.indexOf(url)) {
self.serve(req, res);
} else {
for (var i = 0; i < evs.length; i++) {
evs[i].call(srv, req, res);
}
}
});
};
/**
* Handles a request serving `/socket.io.js`
*
* @param {http.Request} req
* @param {http.Response} res
* @api private
*/
Server.prototype.serve = function(req, res){
if (req.headers.etag) {
if (clientVersion == req.headers.etag) {
debug('serve client 304');
res.writeHead(304);
res.end();
return;
}
}
debug('serve client source');
res.setHeader('Content-Type', 'application/javascript');
res.setHeader('ETag', clientVersion);
res.writeHead(200);
res.end(clientSource);
};
/**
* Binds socket.io to an engine.io instance.
*
* @param {engine.Server} engine.io (or compatible) server
* @return {Server} self
* @api public
*/
Server.prototype.bind = function(engine){
this.engine = engine;
this.engine.on('connection', this.onconnection.bind(this));
return this;
};
/**
* Called with each incoming transport connection.
*
* @param {engine.Socket} socket
* @return {Server} self
* @api public
*/
Server.prototype.onconnection = function(conn){
debug('incoming connection with id %s', conn.id);
var client = new Client(this, conn);
client.connect('/');
return this;
};
/**
* Looks up a namespace.
*
* @param {String} nsp name
* @param {Function} optional, nsp `connection` ev handler
* @api public
*/
Server.prototype.of = function(name, fn){
if (!this.nsps[name]) {
debug('initializing namespace %s', name);
var nsp = new Namespace(this, name);
this.nsps[name] = nsp;
}
if (fn) this.nsps[name].on('connect', fn);
return this.nsps[name];
};
/**
* Expose main namespace (/).
*/
['on', 'to', 'in', 'use', 'emit', 'send', 'write'].forEach(function(fn){
Server.prototype[fn] = function(){
var nsp = this.sockets[fn];
return nsp.apply(this.sockets, arguments);
};
});
Namespace.flags.forEach(function(flag){
Server.prototype.__defineGetter__(flag, function(name){
this.flags.push(name);
return this;
});
});
/**
* BC with `io.listen`
*/
Server.listen = Server;

97
lib/logger.js Normal file
View File

@@ -0,0 +1,97 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var util = require('./util')
, toArray = util.toArray;
/**
* Log levels.
*/
var levels = [
'error'
, 'warn'
, 'info'
, 'debug'
];
/**
* Colors for log levels.
*/
var colors = [
31
, 33
, 36
, 90
];
/**
* Pads the nice output to the longest log level.
*/
function pad (str) {
var max = 0;
for (var i = 0, l = levels.length; i < l; i++)
max = Math.max(max, levels[i].length);
if (str.length < max)
return str + new Array(max - str.length + 1).join(' ');
return str;
};
/**
* Logger (console).
*
* @api public
*/
var Logger = module.exports = function (opts) {
opts = opts || {}
this.colors = false !== opts.colors;
this.level = 3;
this.enabled = true;
};
/**
* Log method.
*
* @api public
*/
Logger.prototype.log = function (type) {
var index = levels.indexOf(type);
if (index > this.level || !this.enabled)
return this;
console.log.apply(
console
, [this.colors
? ' \033[' + colors[index] + 'm' + pad(type) + ' -\033[39m'
: type + ':'
].concat(toArray(arguments).slice(1))
);
return this;
};
/**
* Generate methods.
*/
levels.forEach(function (name) {
Logger.prototype[name] = function () {
this.log.apply(this, [name].concat(toArray(arguments)));
};
});

1027
lib/manager.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,242 +1,355 @@
/**
* Module dependencies.
*/
var Socket = require('./socket');
var Emitter = require('events').EventEmitter;
var parser = require('socket.io-parser');
var debug = require('debug')('socket.io:namespace');
var hasBin = require('has-binary-data');
var Socket = require('./socket')
, EventEmitter = process.EventEmitter
, parser = require('./parser')
, util = require('./util');
/**
* Module exports.
* Exports the constructor.
*/
module.exports = exports = Namespace;
exports = module.exports = SocketNamespace;
/**
* Blacklisted events.
*/
exports.events = [
'connect', // for symmetry with client
'connection',
'newListener'
];
/**
* Flags.
*/
exports.flags = ['json'];
/**
* `EventEmitter#emit` reference.
*/
var emit = Emitter.prototype.emit;
/**
* Namespace constructor.
* Constructor.
*
* @api public.
*/
function SocketNamespace (mgr, name) {
this.manager = mgr;
this.name = name || '';
this.sockets = {};
this.auth = false;
this.setFlags();
};
/**
* Inherits from EventEmitter.
*/
SocketNamespace.prototype.__proto__ = EventEmitter.prototype;
/**
* Copies emit since we override it.
*
* @param {Server} server instance
* @param {Socket} name
* @api private
*/
function Namespace(server, name){
this.name = name;
this.server = server;
this.sockets = [];
this.connected = {};
this.fns = [];
this.ids = 0;
this.acks = {};
this.initAdapter();
}
SocketNamespace.prototype.$emit = EventEmitter.prototype.emit;
/**
* Inherits from `EventEmitter`.
* Retrieves all clients as Socket instances as an array.
*
* @api public
*/
Namespace.prototype.__proto__ = Emitter.prototype;
SocketNamespace.prototype.clients = function (room) {
var room = this.name + (room !== undefined ?
'/' + room : '');
if (!this.manager.rooms[room]) {
return [];
}
return this.manager.rooms[room].map(function (id) {
return this.socket(id);
}, this);
};
/**
* Apply flags from `Socket`.
* Access logger interface.
*
* @api public
*/
exports.flags.forEach(function(flag){
Namespace.prototype.__defineGetter__(flag, function(){
this.flags = this.flags || {};
this.flags[flag] = true;
return this;
});
SocketNamespace.prototype.__defineGetter__('log', function () {
return this.manager.log;
});
/**
* Initializes the `Adapter` for this nsp.
* Run upon changing adapter by `Server#adapter`
* in addition to the constructor.
* Access store.
*
* @api private
*/
Namespace.prototype.initAdapter = function(){
this.adapter = new (this.server.adapter())(this);
};
/**
* Sets up namespace middleware.
*
* @return {Namespace} self
* @api public
*/
Namespace.prototype.use = function(fn){
this.fns.push(fn);
SocketNamespace.prototype.__defineGetter__('store', function () {
return this.manager.store;
});
/**
* JSON message flag.
*
* @api public
*/
SocketNamespace.prototype.__defineGetter__('json', function () {
this.flags.json = true;
return this;
});
/**
* Volatile message flag.
*
* @api public
*/
SocketNamespace.prototype.__defineGetter__('volatile', function () {
this.flags.volatile = true;
return this;
});
/**
* Overrides the room to relay messages to (flag).
*
* @api public
*/
SocketNamespace.prototype.in = SocketNamespace.prototype.to = function (room) {
this.flags.endpoint = this.name + (room ? '/' + room : '');
return this;
};
/**
* Executes the middleware for an incoming client.
* Adds a session id we should prevent relaying messages to (flag).
*
* @param {Socket} socket that will get added
* @param {Function} last fn call in the middleware
* @api private
*/
Namespace.prototype.run = function(socket, fn){
var fns = this.fns.slice(0);
if (!fns.length) return fn(null);
function run(i){
fns[i](socket, function(err){
// upon error, short-circuit
if (err) return fn(err);
// if no middleware left, summon callback
if (!fns[i + 1]) return fn(null);
// go on to next
run(i + 1);
});
}
run(0);
};
/**
* Targets a room when emitting.
*
* @param {String} name
* @return {Namespace} self
* @api public
*/
Namespace.prototype.to =
Namespace.prototype['in'] = function(name){
this.rooms = this.rooms || [];
if (!~this.rooms.indexOf(name)) this.rooms.push(name);
SocketNamespace.prototype.except = function (id) {
this.flags.exceptions.push(id);
return this;
};
/**
* Adds a new client.
* Sets the default flags.
*
* @return {Socket}
* @api private
*/
Namespace.prototype.add = function(client, fn){
debug('adding socket to nsp %s', this.name);
var socket = new Socket(this, client);
var self = this;
this.run(socket, function(err){
process.nextTick(function(){
if ('open' == client.conn.readyState) {
if (err) return socket.error(err.data || err.message);
SocketNamespace.prototype.setFlags = function () {
this.flags = {
endpoint: this.name
, exceptions: []
};
return this;
};
// track socket
self.sockets.push(socket);
/**
* Sends out a packet.
*
* @api private
*/
// it's paramount that the internal `onconnect` logic
// fires before user-set events to prevent state order
// violations (such as a disconnection before the connection
// logic is complete)
socket.onconnect();
if (fn) fn();
SocketNamespace.prototype.packet = function (packet) {
packet.endpoint = this.name;
// fire user-set events
self.emit('connect', socket);
self.emit('connection', socket);
} else {
debug('next called after client was closed - ignoring socket');
}
});
var store = this.store
, log = this.log
, volatile = this.flags.volatile
, exceptions = this.flags.exceptions
, packet = parser.encodePacket(packet);
this.manager.onDispatch(this.flags.endpoint, packet, volatile, exceptions);
this.store.publish('dispatch', this.flags.endpoint, packet, volatile, exceptions);
this.setFlags();
return this;
};
/**
* Sends to everyone.
*
* @api public
*/
SocketNamespace.prototype.send = function (data) {
return this.packet({
type: this.flags.json ? 'json' : 'message'
, data: data
});
return socket;
};
/**
* Removes a client. Called by each `Socket`.
* Emits to everyone (override).
*
* @api public
*/
SocketNamespace.prototype.emit = function (name) {
if (name == 'newListener') {
return this.$emit.apply(this, arguments);
}
return this.packet({
type: 'event'
, name: name
, args: util.toArray(arguments).slice(1)
});
};
/**
* Retrieves or creates a write-only socket for a client, unless specified.
*
* @param {Boolean} whether the socket will be readable when initialized
* @api public
*/
SocketNamespace.prototype.socket = function (sid, readable) {
if (!this.sockets[sid]) {
this.sockets[sid] = new Socket(this.manager, sid, this, readable);
}
return this.sockets[sid];
};
/**
* Sets authorization for this namespace.
*
* @api public
*/
SocketNamespace.prototype.authorization = function (fn) {
this.auth = fn;
return this;
};
/**
* Called when a socket disconnects entirely.
*
* @api private
*/
Namespace.prototype.remove = function(socket){
var i = this.sockets.indexOf(socket);
if (~i) {
this.sockets.splice(i, 1);
} else {
debug('ignoring remove for %s', socket.id);
SocketNamespace.prototype.handleDisconnect = function (sid, reason, raiseOnDisconnect) {
if (this.sockets[sid] && this.sockets[sid].readable) {
if (raiseOnDisconnect) this.sockets[sid].onDisconnect(reason);
delete this.sockets[sid];
}
};
/**
* Emits to all clients.
* Performs authentication.
*
* @return {Namespace} self
* @api public
* @param Object client request data
* @api private
*/
Namespace.prototype.emit = function(ev){
if (~exports.events.indexOf(ev)) {
emit.apply(this, arguments);
} else {
// set up packet object
var args = Array.prototype.slice.call(arguments);
var parserType = parser.EVENT; // default
if (hasBin(args)) { parserType = parser.BINARY_EVENT; } // binary
SocketNamespace.prototype.authorize = function (data, fn) {
if (this.auth) {
var self = this;
var packet = { type: parserType, data: args };
if ('function' == typeof args[args.length - 1]) {
throw new Error('Callbacks are not supported when broadcasting');
}
this.adapter.broadcast(packet, {
rooms: this.rooms,
flags: this.flags
this.auth.call(this, data, function (err, authorized) {
self.log.debug('client ' +
(authorized ? '' : 'un') + 'authorized for ' + self.name);
fn(err, authorized);
});
delete this.rooms;
delete this.flags;
} else {
this.log.debug('client authorized for ' + this.name);
fn(null, true);
}
return this;
};
/**
* Sends a `message` event to all clients.
* Handles a packet.
*
* @return {Namespace} self
* @api public
* @api private
*/
Namespace.prototype.send =
Namespace.prototype.write = function(){
var args = Array.prototype.slice.call(arguments);
args.unshift('message');
this.emit.apply(this, args);
return this;
SocketNamespace.prototype.handlePacket = function (sessid, packet) {
var socket = this.socket(sessid)
, dataAck = packet.ack == 'data'
, manager = this.manager
, self = this;
function ack () {
self.log.debug('sending data ack packet');
socket.packet({
type: 'ack'
, args: util.toArray(arguments)
, ackId: packet.id
});
};
function error (err) {
self.log.warn('handshake error ' + err + ' for ' + self.name);
socket.packet({ type: 'error', reason: err });
};
function connect () {
self.manager.onJoin(sessid, self.name);
self.store.publish('join', sessid, self.name);
// packet echo
socket.packet({ type: 'connect' });
// emit connection event
self.$emit('connection', socket);
};
switch (packet.type) {
case 'connect':
if (packet.endpoint == '') {
connect();
} else {
var handshakeData = manager.handshaken[sessid];
this.authorize(handshakeData, function (err, authorized, newData) {
if (err) return error(err);
if (authorized) {
manager.onHandshake(sessid, newData || handshakeData);
self.store.publish('handshake', sessid, newData || handshakeData);
connect();
} else {
error('unauthorized');
}
});
}
break;
case 'ack':
if (socket.acks[packet.ackId]) {
socket.acks[packet.ackId].apply(socket, packet.args);
} else {
this.log.info('unknown ack packet');
}
break;
case 'event':
// check if the emitted event is not blacklisted
if (-~manager.get('blacklist').indexOf(packet.name)) {
this.log.debug('ignoring blacklisted event `' + packet.name + '`');
} else {
var params = [packet.name].concat(packet.args);
if (dataAck) {
params.push(ack);
}
socket.$emit.apply(socket, params);
}
break;
case 'disconnect':
this.manager.onLeave(sessid, this.name);
this.store.publish('leave', sessid, this.name);
socket.$emit('disconnect', packet.reason || 'packet');
break;
case 'json':
case 'message':
var params = ['message', packet.data];
if (dataAck)
params.push(ack);
socket.$emit.apply(socket, params);
};
};

249
lib/parser.js Normal file
View File

@@ -0,0 +1,249 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
/**
* Packet types.
*/
var packets = exports.packets = {
'disconnect': 0
, 'connect': 1
, 'heartbeat': 2
, 'message': 3
, 'json': 4
, 'event': 5
, 'ack': 6
, 'error': 7
, 'noop': 8
}
, packetslist = Object.keys(packets);
/**
* Errors reasons.
*/
var reasons = exports.reasons = {
'transport not supported': 0
, 'client not handshaken': 1
, 'unauthorized': 2
}
, reasonslist = Object.keys(reasons);
/**
* Errors advice.
*/
var advice = exports.advice = {
'reconnect': 0
}
, advicelist = Object.keys(advice);
/**
* Encodes a packet.
*
* @api private
*/
exports.encodePacket = function (packet) {
var type = packets[packet.type]
, id = packet.id || ''
, endpoint = packet.endpoint || ''
, ack = packet.ack
, data = null;
switch (packet.type) {
case 'message':
if (packet.data !== '')
data = packet.data;
break;
case 'event':
var ev = { name: packet.name };
if (packet.args && packet.args.length) {
ev.args = packet.args;
}
data = JSON.stringify(ev);
break;
case 'json':
data = JSON.stringify(packet.data);
break;
case 'ack':
data = packet.ackId
+ (packet.args && packet.args.length
? '+' + JSON.stringify(packet.args) : '');
break;
case 'connect':
if (packet.qs)
data = packet.qs;
break;
case 'error':
var reason = packet.reason ? reasons[packet.reason] : ''
, adv = packet.advice ? advice[packet.advice] : ''
if (reason !== '' || adv !== '')
data = reason + (adv !== '' ? ('+' + adv) : '')
break;
}
// construct packet with required fragments
var encoded = type + ':' + id + (ack == 'data' ? '+' : '') + ':' + endpoint;
// data fragment is optional
if (data !== null && data !== undefined)
encoded += ':' + data;
return encoded;
};
/**
* Encodes multiple messages (payload).
*
* @param {Array} messages
* @api private
*/
exports.encodePayload = function (packets) {
var decoded = '';
if (packets.length == 1)
return packets[0];
for (var i = 0, l = packets.length; i < l; i++) {
var packet = packets[i];
decoded += '\ufffd' + packet.length + '\ufffd' + packets[i]
}
return decoded;
};
/**
* Decodes a packet
*
* @api private
*/
var regexp = /([^:]+):([0-9]+)?(\+)?:([^:]+)?:?([\s\S]*)?/;
/**
* Wrap the JSON.parse in a seperate function the crankshaft optimizer will
* only punish this function for the usage for try catch
*
* @api private
*/
function parse (data) {
try { return JSON.parse(data) }
catch (e) { return false }
}
exports.decodePacket = function (data) {
var pieces = data.match(regexp);
if (!pieces) return {};
var id = pieces[2] || ''
, data = pieces[5] || ''
, packet = {
type: packetslist[pieces[1]]
, endpoint: pieces[4] || ''
};
// whether we need to acknowledge the packet
if (id) {
packet.id = id;
if (pieces[3])
packet.ack = 'data';
else
packet.ack = true;
}
// handle different packet types
switch (packet.type) {
case 'message':
packet.data = data || '';
break;
case 'event':
pieces = parse(data);
if (pieces) {
packet.name = pieces.name;
packet.args = pieces.args;
}
packet.args = packet.args || [];
break;
case 'json':
packet.data = parse(data);
break;
case 'connect':
packet.qs = data || '';
break;
case 'ack':
pieces = data.match(/^([0-9]+)(\+)?(.*)/);
if (pieces) {
packet.ackId = pieces[1];
packet.args = [];
if (pieces[3]) {
packet.args = parse(pieces[3]) || [];
}
}
break;
case 'error':
pieces = data.split('+');
packet.reason = reasonslist[pieces[0]] || '';
packet.advice = advicelist[pieces[1]] || '';
}
return packet;
};
/**
* Decodes data payload. Detects multiple messages
*
* @return {Array} messages
* @api public
*/
exports.decodePayload = function (data) {
if (undefined == data || null == data) {
return [];
}
if (data[0] == '\ufffd') {
var ret = [];
for (var i = 1, length = ''; i < data.length; i++) {
if (data[i] == '\ufffd') {
ret.push(exports.decodePacket(data.substr(i + 1, length)));
i += Number(length) + 1;
length = '';
} else {
length += data[i];
}
}
return ret;
} else {
return [exports.decodePacket(data)];
}
};

143
lib/socket.io.js Normal file
View File

@@ -0,0 +1,143 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var client = require('socket.io-client');
/**
* Version.
*/
exports.version = '0.9.15';
/**
* Supported protocol version.
*/
exports.protocol = 1;
/**
* Client that we serve.
*/
exports.clientVersion = client.version;
/**
* Attaches a manager
*
* @param {HTTPServer/Number} a HTTP/S server or a port number to listen on.
* @param {Object} opts to be passed to Manager and/or http server
* @param {Function} callback if a port is supplied
* @api public
*/
exports.listen = function (server, options, fn) {
if ('function' == typeof server) {
console.warn('Socket.IO\'s `listen()` method expects an `http.Server` instance\n'
+ 'as its first parameter. Are you migrating from Express 2.x to 3.x?\n'
+ 'If so, check out the "Socket.IO compatibility" section at:\n'
+ 'https://github.com/visionmedia/express/wiki/Migrating-from-2.x-to-3.x');
}
if ('function' == typeof options) {
fn = options;
options = {};
}
if ('undefined' == typeof server) {
// create a server that listens on port 80
server = 80;
}
if ('number' == typeof server) {
// if a port number is passed
var port = server;
if (options && options.key)
server = require('https').createServer(options);
else
server = require('http').createServer();
// default response
server.on('request', function (req, res) {
res.writeHead(200);
res.end('Welcome to socket.io.');
});
server.listen(port, fn);
}
// otherwise assume a http/s server
return new exports.Manager(server, options);
};
/**
* Manager constructor.
*
* @api public
*/
exports.Manager = require('./manager');
/**
* Transport constructor.
*
* @api public
*/
exports.Transport = require('./transport');
/**
* Socket constructor.
*
* @api public
*/
exports.Socket = require('./socket');
/**
* Static constructor.
*
* @api public
*/
exports.Static = require('./static');
/**
* Store constructor.
*
* @api public
*/
exports.Store = require('./store');
/**
* Memory Store constructor.
*
* @api public
*/
exports.MemoryStore = require('./stores/memory');
/**
* Redis Store constructor.
*
* @api public
*/
exports.RedisStore = require('./stores/redis');
/**
* Parser.
*
* @api public
*/
exports.parser = require('./parser');

View File

@@ -1,422 +1,369 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var Emitter = require('events').EventEmitter;
var parser = require('socket.io-parser');
var url = require('url');
var debug = require('debug')('socket.io:socket');
var hasBin = require('has-binary-data');
var parser = require('./parser')
, util = require('./util')
, EventEmitter = process.EventEmitter
/**
* Module exports.
* Export the constructor.
*/
module.exports = exports = Socket;
exports = module.exports = Socket;
/**
* Blacklisted events.
* Default error event listener to prevent uncaught exceptions.
*/
var defaultError = function () {};
/**
* Socket constructor.
*
* @param {Manager} manager instance
* @param {String} session id
* @param {Namespace} namespace the socket belongs to
* @param {Boolean} whether the
* @api public
*/
exports.events = [
'error',
'connect',
'disconnect',
'newListener'
];
/**
* Flags.
*
* @api private
*/
var flags = [
'json',
'volatile',
'broadcast'
];
/**
* `EventEmitter#emit` reference.
*/
var emit = Emitter.prototype.emit;
/**
* Interface to a `Client` for a given `Namespace`.
*
* @param {Namespace} nsp
* @param {Client} client
* @api public
*/
function Socket(nsp, client){
this.nsp = nsp;
this.server = nsp.server;
this.adapter = this.nsp.adapter;
this.id = client.id;
this.request = client.request;
this.client = client;
this.conn = client.conn;
this.rooms = [];
this.acks = {};
this.connected = true;
function Socket (manager, id, nsp, readable) {
this.id = id;
this.namespace = nsp;
this.manager = manager;
this.disconnected = false;
this.handshake = this.buildHandshake();
}
this.ackPackets = 0;
this.acks = {};
this.setFlags();
this.readable = readable;
this.store = this.manager.store.client(this.id);
this.on('error', defaultError);
};
/**
* Inherits from `EventEmitter`.
* Inherits from EventEmitter.
*/
Socket.prototype.__proto__ = Emitter.prototype;
Socket.prototype.__proto__ = EventEmitter.prototype;
/**
* Apply flags from `Socket`.
*/
flags.forEach(function(flag){
Socket.prototype.__defineGetter__(flag, function(){
this.flags = this.flags || {};
this.flags[flag] = true;
return this;
});
});
/**
* `request` engine.io shorcut.
*
* @api public
*/
Socket.prototype.__defineGetter__('request', function(){
return this.conn.request;
});
/**
* Builds the `handshake` BC object
* Accessor shortcut for the handshake data
*
* @api private
*/
Socket.prototype.buildHandshake = function(){
return {
headers: this.request.headers,
time: (new Date) + '',
address: this.request.connection.address(),
xdomain: !!this.request.headers.origin,
secure: !!this.request.connection.encrypted,
issued: +(new Date),
url: this.request.url,
query: url.parse(this.request.url, true).query || {}
};
};
Socket.prototype.__defineGetter__('handshake', function () {
return this.manager.handshaken[this.id];
});
/**
* Emits to this client.
* Accessor shortcut for the transport type
*
* @api private
*/
Socket.prototype.__defineGetter__('transport', function () {
return this.manager.transports[this.id].name;
});
/**
* Accessor shortcut for the logger.
*
* @api private
*/
Socket.prototype.__defineGetter__('log', function () {
return this.manager.log;
});
/**
* JSON message flag.
*
* @return {Socket} self
* @api public
*/
Socket.prototype.emit = function(ev){
if (~exports.events.indexOf(ev)) {
emit.apply(this, arguments);
} else {
var args = Array.prototype.slice.call(arguments);
var packet = {};
packet.type = hasBin(args) ? parser.BINARY_EVENT : parser.EVENT;
packet.data = args;
Socket.prototype.__defineGetter__('json', function () {
this.flags.json = true;
return this;
});
// access last argument to see if it's an ACK callback
if ('function' == typeof args[args.length - 1]) {
if (this._rooms || (this.flags && this.flags.broadcast)) {
throw new Error('Callbacks are not supported when broadcasting');
/**
* Volatile message flag.
*
* @api public
*/
Socket.prototype.__defineGetter__('volatile', function () {
this.flags.volatile = true;
return this;
});
/**
* Broadcast message flag.
*
* @api public
*/
Socket.prototype.__defineGetter__('broadcast', function () {
this.flags.broadcast = true;
return this;
});
/**
* Overrides the room to broadcast messages to (flag)
*
* @api public
*/
Socket.prototype.to = Socket.prototype.in = function (room) {
this.flags.room = room;
return this;
};
/**
* Resets flags
*
* @api private
*/
Socket.prototype.setFlags = function () {
this.flags = {
endpoint: this.namespace.name
, room: ''
};
return this;
};
/**
* Triggered on disconnect
*
* @api private
*/
Socket.prototype.onDisconnect = function (reason) {
if (!this.disconnected) {
this.$emit('disconnect', reason);
this.disconnected = true;
}
};
/**
* Joins a user to a room.
*
* @api public
*/
Socket.prototype.join = function (name, fn) {
var nsp = this.namespace.name
, name = (nsp + '/') + name;
this.manager.onJoin(this.id, name);
this.manager.store.publish('join', this.id, name);
if (fn) {
this.log.warn('Client#join callback is deprecated');
fn();
}
return this;
};
/**
* Un-joins a user from a room.
*
* @api public
*/
Socket.prototype.leave = function (name, fn) {
var nsp = this.namespace.name
, name = (nsp + '/') + name;
this.manager.onLeave(this.id, name);
this.manager.store.publish('leave', this.id, name);
if (fn) {
this.log.warn('Client#leave callback is deprecated');
fn();
}
return this;
};
/**
* Transmits a packet.
*
* @api private
*/
Socket.prototype.packet = function (packet) {
if (this.flags.broadcast) {
this.log.debug('broadcasting packet');
this.namespace.in(this.flags.room).except(this.id).packet(packet);
} else {
packet.endpoint = this.flags.endpoint;
packet = parser.encodePacket(packet);
this.dispatch(packet, this.flags.volatile);
}
this.setFlags();
return this;
};
/**
* Dispatches a packet
*
* @api private
*/
Socket.prototype.dispatch = function (packet, volatile) {
if (this.manager.transports[this.id] && this.manager.transports[this.id].open) {
this.manager.transports[this.id].onDispatch(packet, volatile);
} else {
if (!volatile) {
this.manager.onClientDispatch(this.id, packet, volatile);
}
this.manager.store.publish('dispatch:' + this.id, packet, volatile);
}
};
/**
* Stores data for the client.
*
* @api public
*/
Socket.prototype.set = function (key, value, fn) {
this.store.set(key, value, fn);
return this;
};
/**
* Retrieves data for the client
*
* @api public
*/
Socket.prototype.get = function (key, fn) {
this.store.get(key, fn);
return this;
};
/**
* Checks data for the client
*
* @api public
*/
Socket.prototype.has = function (key, fn) {
this.store.has(key, fn);
return this;
};
/**
* Deletes data for the client
*
* @api public
*/
Socket.prototype.del = function (key, fn) {
this.store.del(key, fn);
return this;
};
/**
* Kicks client
*
* @api public
*/
Socket.prototype.disconnect = function () {
if (!this.disconnected) {
this.log.info('booting client');
if ('' === this.namespace.name) {
if (this.manager.transports[this.id] && this.manager.transports[this.id].open) {
this.manager.transports[this.id].onForcedDisconnect();
} else {
this.manager.onClientDisconnect(this.id);
this.manager.store.publish('disconnect:' + this.id);
}
debug('emitting packet with ack id %d', this.nsp.ids);
this.acks[this.nsp.ids] = args.pop();
packet.id = this.nsp.ids++;
}
if (this._rooms || (this.flags && this.flags.broadcast)) {
this.adapter.broadcast(packet, {
except: [this.id],
rooms: this._rooms,
flags: this.flags
});
} else {
// dispatch packet
this.packet(packet);
this.packet({type: 'disconnect'});
this.manager.onLeave(this.id, this.namespace.name);
this.$emit('disconnect', 'booted');
}
// reset flags
delete this._rooms;
delete this.flags;
}
return this;
};
/**
* Targets a room when broadcasting.
* Send a message.
*
* @param {String} name
* @return {Socket} self
* @api public
*/
Socket.prototype.to =
Socket.prototype.in = function(name){
this._rooms = this._rooms || [];
if (!~this._rooms.indexOf(name)) this._rooms.push(name);
return this;
};
/**
* Sends a `message` event.
*
* @return {Socket} self
* @api public
*/
Socket.prototype.send =
Socket.prototype.write = function(){
var args = Array.prototype.slice.call(arguments);
args.unshift('message');
this.emit.apply(this, args);
return this;
};
/**
* Writes a packet.
*
* @param {Object} packet object
* @api private
*/
Socket.prototype.packet = function(packet, preEncoded){
packet.nsp = this.nsp.name;
var volatile = this.flags && this.flags.volatile;
this.client.packet(packet, preEncoded, volatile);
};
/**
* Joins a room.
*
* @param {String} room
* @param {Function} optional, callback
* @return {Socket} self
* @api private
*/
Socket.prototype.join = function(room, fn){
debug('joining room %s', room);
var self = this;
if (~this.rooms.indexOf(room)) return this;
this.adapter.add(this.id, room, function(err){
if (err) return fn && fn(err);
debug('joined room %s', room);
self.rooms.push(room);
fn && fn(null);
});
return this;
};
/**
* Leaves a room.
*
* @param {String} room
* @param {Function} optional, callback
* @return {Socket} self
* @api private
*/
Socket.prototype.leave = function(room, fn){
debug('leave room %s', room);
var self = this;
this.adapter.del(this.id, room, function(err){
if (err) return fn && fn(err);
debug('left room %s', room);
self.rooms.splice(self.rooms.indexOf(room, 1));
fn && fn(null);
});
return this;
};
/**
* Leave all rooms.
*
* @api private
*/
Socket.prototype.leaveAll = function(){
this.adapter.delAll(this.id);
};
/**
* Called by `Namespace` upon succesful
* middleware execution (ie: authorization).
*
* @api private
*/
Socket.prototype.onconnect = function(){
debug('socket connected - writing packet');
this.join(this.id);
this.packet({ type: parser.CONNECT });
this.nsp.connected[this.id] = this;
};
/**
* Called with each packet. Called by `Client`.
*
* @param {Object} packet
* @api private
*/
Socket.prototype.onpacket = function(packet){
debug('got packet %j', packet);
switch (packet.type) {
case parser.EVENT:
this.onevent(packet);
break;
case parser.BINARY_EVENT:
this.onevent(packet);
break;
case parser.ACK:
this.onack(packet);
break;
case parser.DISCONNECT:
this.ondisconnect();
break;
case parser.ERROR:
this.emit('error', packet.data);
}
};
/**
* Called upon event packet.
*
* @param {Object} packet object
* @api private
*/
Socket.prototype.onevent = function(packet){
var args = packet.data || [];
debug('emitting event %j', args);
if (null != packet.id) {
debug('attaching ack callback to event');
args.push(this.ack(packet.id));
}
emit.apply(this, args);
};
/**
* Produces an ack callback to emit with an event.
*
* @param {Number} packet id
* @api private
*/
Socket.prototype.ack = function(id){
var self = this;
var sent = false;
return function(){
// prevent double callbacks
if (sent) return;
var args = Array.prototype.slice.call(arguments);
debug('sending ack %j', args);
self.packet({
id: id,
type: parser.ACK,
data: args
});
Socket.prototype.send = function (data, fn) {
var packet = {
type: this.flags.json ? 'json' : 'message'
, data: data
};
};
/**
* Called upon ack packet.
*
* @api private
*/
Socket.prototype.onack = function(packet){
var ack = this.acks[packet.id];
if ('function' == typeof ack) {
debug('calling ack %s with %j', packet.id, packet.data);
ack.apply(this, packet.data);
delete this.acks[packet.id];
} else {
debug('bad ack %s', packet.id);
if (fn) {
packet.id = ++this.ackPackets;
packet.ack = true;
this.acks[packet.id] = fn;
}
return this.packet(packet);
};
/**
* Called upon client disconnect packet.
* Original emit function.
*
* @api private
*/
Socket.prototype.ondisconnect = function(){
debug('got disconnect packet');
this.onclose('client namespace disconnect');
};
Socket.prototype.$emit = EventEmitter.prototype.emit;
/**
* Called upon closing. Called by `Client`.
* Emit override for custom events.
*
* @param {String} reason
* @api private
*/
Socket.prototype.onclose = function(reason){
if (!this.connected) return this;
debug('closing socket - reason %s', reason);
this.leaveAll();
this.nsp.remove(this);
this.client.remove(this);
this.connected = false;
this.disconnected = true;
delete this.nsp.connected[this.id];
this.emit('disconnect', reason);
};
/**
* Produces an `error` packet.
*
* @param {Object} error object
* @api private
*/
Socket.prototype.error = function(err){
this.packet({ type: parser.ERROR, data: err });
};
/**
* Disconnects this client.
*
* @param {Boolean} if `true`, closes the underlying connection
* @return {Socket} self
* @api public
*/
Socket.prototype.disconnect = function(close){
if (!this.connected) return this;
if (close) {
this.client.disconnect();
} else {
this.packet({ type: parser.DISCONNECT });
this.onclose('server namespace disconnect');
Socket.prototype.emit = function (ev) {
if (ev == 'newListener') {
return this.$emit.apply(this, arguments);
}
return this;
var args = util.toArray(arguments).slice(1)
, lastArg = args[args.length - 1]
, packet = {
type: 'event'
, name: ev
};
if ('function' == typeof lastArg) {
packet.id = ++this.ackPackets;
packet.ack = lastArg.length ? 'data' : true;
this.acks[packet.id] = lastArg;
args = args.slice(0, args.length - 1);
}
packet.args = args;
return this.packet(packet);
};

395
lib/static.js Normal file
View File

@@ -0,0 +1,395 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var client = require('socket.io-client')
, cp = require('child_process')
, fs = require('fs')
, util = require('./util');
/**
* File type details.
*
* @api private
*/
var mime = {
js: {
type: 'application/javascript'
, encoding: 'utf8'
, gzip: true
}
, swf: {
type: 'application/x-shockwave-flash'
, encoding: 'binary'
, gzip: false
}
};
/**
* Regexp for matching custom transport patterns. Users can configure their own
* socket.io bundle based on the url structure. Different transport names are
* concatinated using the `+` char. /socket.io/socket.io+websocket.js should
* create a bundle that only contains support for the websocket.
*
* @api private
*/
var bundle = /\+((?:\+)?[\w\-]+)*(?:\.v\d+\.\d+\.\d+)?(?:\.js)$/
, versioning = /\.v\d+\.\d+\.\d+(?:\.js)$/;
/**
* Export the constructor
*/
exports = module.exports = Static;
/**
* Static constructor
*
* @api public
*/
function Static (manager) {
this.manager = manager;
this.cache = {};
this.paths = {};
this.init();
}
/**
* Initialize the Static by adding default file paths.
*
* @api public
*/
Static.prototype.init = function () {
/**
* Generates a unique id based the supplied transports array
*
* @param {Array} transports The array with transport types
* @api private
*/
function id (transports) {
var id = transports.join('').split('').map(function (char) {
return ('' + char.charCodeAt(0)).split('').pop();
}).reduce(function (char, id) {
return char +id;
});
return client.version + ':' + id;
}
/**
* Generates a socket.io-client file based on the supplied transports.
*
* @param {Array} transports The array with transport types
* @param {Function} callback Callback for the static.write
* @api private
*/
function build (transports, callback) {
client.builder(transports, {
minify: self.manager.enabled('browser client minification')
}, function (err, content) {
callback(err, content ? new Buffer(content) : null, id(transports));
}
);
}
var self = this;
// add our default static files
this.add('/static/flashsocket/WebSocketMain.swf', {
file: client.dist + '/WebSocketMain.swf'
});
this.add('/static/flashsocket/WebSocketMainInsecure.swf', {
file: client.dist + '/WebSocketMainInsecure.swf'
});
// generates dedicated build based on the available transports
this.add('/socket.io.js', function (path, callback) {
build(self.manager.get('transports'), callback);
});
this.add('/socket.io.v', { mime: mime.js }, function (path, callback) {
build(self.manager.get('transports'), callback);
});
// allow custom builds based on url paths
this.add('/socket.io+', { mime: mime.js }, function (path, callback) {
var available = self.manager.get('transports')
, matches = path.match(bundle)
, transports = [];
if (!matches) return callback('No valid transports');
// make sure they valid transports
matches[0].split('.')[0].split('+').slice(1).forEach(function (transport) {
if (!!~available.indexOf(transport)) {
transports.push(transport);
}
});
if (!transports.length) return callback('No valid transports');
build(transports, callback);
});
// clear cache when transports change
this.manager.on('set:transports', function (key, value) {
delete self.cache['/socket.io.js'];
Object.keys(self.cache).forEach(function (key) {
if (bundle.test(key)) {
delete self.cache[key];
}
});
});
};
/**
* Gzip compress buffers.
*
* @param {Buffer} data The buffer that needs gzip compression
* @param {Function} callback
* @api public
*/
Static.prototype.gzip = function (data, callback) {
var gzip = cp.spawn('gzip', ['-9', '-c', '-f', '-n'])
, encoding = Buffer.isBuffer(data) ? 'binary' : 'utf8'
, buffer = []
, err;
gzip.stdout.on('data', function (data) {
buffer.push(data);
});
gzip.stderr.on('data', function (data) {
err = data +'';
buffer.length = 0;
});
gzip.on('close', function () {
if (err) return callback(err);
var size = 0
, index = 0
, i = buffer.length
, content;
while (i--) {
size += buffer[i].length;
}
content = new Buffer(size);
i = buffer.length;
buffer.forEach(function (buffer) {
var length = buffer.length;
buffer.copy(content, index, 0, length);
index += length;
});
buffer.length = 0;
callback(null, content);
});
gzip.stdin.end(data, encoding);
};
/**
* Is the path a static file?
*
* @param {String} path The path that needs to be checked
* @api public
*/
Static.prototype.has = function (path) {
// fast case
if (this.paths[path]) return this.paths[path];
var keys = Object.keys(this.paths)
, i = keys.length;
while (i--) {
if (-~path.indexOf(keys[i])) return this.paths[keys[i]];
}
return false;
};
/**
* Add new paths new paths that can be served using the static provider.
*
* @param {String} path The path to respond to
* @param {Options} options Options for writing out the response
* @param {Function} [callback] Optional callback if no options.file is
* supplied this would be called instead.
* @api public
*/
Static.prototype.add = function (path, options, callback) {
var extension = /(?:\.(\w{1,4}))$/.exec(path);
if (!callback && typeof options == 'function') {
callback = options;
options = {};
}
options.mime = options.mime || (extension ? mime[extension[1]] : false);
if (callback) options.callback = callback;
if (!(options.file || options.callback) || !options.mime) return false;
this.paths[path] = options;
return true;
};
/**
* Writes a static response.
*
* @param {String} path The path for the static content
* @param {HTTPRequest} req The request object
* @param {HTTPResponse} res The response object
* @api public
*/
Static.prototype.write = function (path, req, res) {
/**
* Write a response without throwing errors because can throw error if the
* response is no longer writable etc.
*
* @api private
*/
function write (status, headers, content, encoding) {
try {
res.writeHead(status, headers || undefined);
// only write content if it's not a HEAD request and we actually have
// some content to write (304's doesn't have content).
res.end(
req.method !== 'HEAD' && content ? content : ''
, encoding || undefined
);
} catch (e) {}
}
/**
* Answers requests depending on the request properties and the reply object.
*
* @param {Object} reply The details and content to reply the response with
* @api private
*/
function answer (reply) {
var cached = req.headers['if-none-match'] === reply.etag;
if (cached && self.manager.enabled('browser client etag')) {
return write(304);
}
var accept = req.headers['accept-encoding'] || ''
, gzip = !!~accept.toLowerCase().indexOf('gzip')
, mime = reply.mime
, versioned = reply.versioned
, headers = {
'Content-Type': mime.type
};
// check if we can add a etag
if (self.manager.enabled('browser client etag') && reply.etag && !versioned) {
headers['Etag'] = reply.etag;
}
// see if we need to set Expire headers because the path is versioned
if (versioned) {
var expires = self.manager.get('browser client expires');
headers['Cache-Control'] = 'private, x-gzip-ok="", max-age=' + expires;
headers['Date'] = new Date().toUTCString();
headers['Expires'] = new Date(Date.now() + (expires * 1000)).toUTCString();
}
if (gzip && reply.gzip) {
headers['Content-Length'] = reply.gzip.length;
headers['Content-Encoding'] = 'gzip';
headers['Vary'] = 'Accept-Encoding';
write(200, headers, reply.gzip.content, mime.encoding);
} else {
headers['Content-Length'] = reply.length;
write(200, headers, reply.content, mime.encoding);
}
self.manager.log.debug('served static content ' + path);
}
var self = this
, details;
// most common case first
if (this.manager.enabled('browser client cache') && this.cache[path]) {
return answer(this.cache[path]);
} else if (this.manager.get('browser client handler')) {
return this.manager.get('browser client handler').call(this, req, res);
} else if ((details = this.has(path))) {
/**
* A small helper function that will let us deal with fs and dynamic files
*
* @param {Object} err Optional error
* @param {Buffer} content The data
* @api private
*/
function ready (err, content, etag) {
if (err) {
self.manager.log.warn('Unable to serve file. ' + (err.message || err));
return write(500, null, 'Error serving static ' + path);
}
// store the result in the cache
var reply = self.cache[path] = {
content: content
, length: content.length
, mime: details.mime
, etag: etag || client.version
, versioned: versioning.test(path)
};
// check if gzip is enabled
if (details.mime.gzip && self.manager.enabled('browser client gzip')) {
self.gzip(content, function (err, content) {
if (!err) {
reply.gzip = {
content: content
, length: content.length
}
}
answer(reply);
});
} else {
answer(reply);
}
}
if (details.file) {
fs.readFile(details.file, ready);
} else if(details.callback) {
details.callback.call(this, path, ready);
} else {
write(404, null, 'File handle not found');
}
} else {
write(404, null, 'File not found');
}
};

98
lib/store.js Normal file
View File

@@ -0,0 +1,98 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Expose the constructor.
*/
exports = module.exports = Store;
/**
* Module dependencies.
*/
var EventEmitter = process.EventEmitter;
/**
* Store interface
*
* @api public
*/
function Store (options) {
this.options = options;
this.clients = {};
};
/**
* Inherit from EventEmitter.
*/
Store.prototype.__proto__ = EventEmitter.prototype;
/**
* Initializes a client store
*
* @param {String} id
* @api public
*/
Store.prototype.client = function (id) {
if (!this.clients[id]) {
this.clients[id] = new (this.constructor.Client)(this, id);
}
return this.clients[id];
};
/**
* Destroys a client
*
* @api {String} sid
* @param {Number} number of seconds to expire client data
* @api private
*/
Store.prototype.destroyClient = function (id, expiration) {
if (this.clients[id]) {
this.clients[id].destroy(expiration);
delete this.clients[id];
}
return this;
};
/**
* Destroys the store
*
* @param {Number} number of seconds to expire client data
* @api private
*/
Store.prototype.destroy = function (clientExpiration) {
var keys = Object.keys(this.clients)
, count = keys.length;
for (var i = 0, l = count; i < l; i++) {
this.destroyClient(keys[i], clientExpiration);
}
this.clients = {};
return this;
};
/**
* Client.
*
* @api public
*/
Store.Client = function (store, id) {
this.store = store;
this.id = id;
};

143
lib/stores/memory.js Normal file
View File

@@ -0,0 +1,143 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var crypto = require('crypto')
, Store = require('../store');
/**
* Exports the constructor.
*/
exports = module.exports = Memory;
Memory.Client = Client;
/**
* Memory store
*
* @api public
*/
function Memory (opts) {
Store.call(this, opts);
};
/**
* Inherits from Store.
*/
Memory.prototype.__proto__ = Store.prototype;
/**
* Publishes a message.
*
* @api private
*/
Memory.prototype.publish = function () { };
/**
* Subscribes to a channel
*
* @api private
*/
Memory.prototype.subscribe = function () { };
/**
* Unsubscribes
*
* @api private
*/
Memory.prototype.unsubscribe = function () { };
/**
* Client constructor
*
* @api private
*/
function Client () {
Store.Client.apply(this, arguments);
this.data = {};
};
/**
* Inherits from Store.Client
*/
Client.prototype.__proto__ = Store.Client;
/**
* Gets a key
*
* @api public
*/
Client.prototype.get = function (key, fn) {
fn(null, this.data[key] === undefined ? null : this.data[key]);
return this;
};
/**
* Sets a key
*
* @api public
*/
Client.prototype.set = function (key, value, fn) {
this.data[key] = value;
fn && fn(null);
return this;
};
/**
* Has a key
*
* @api public
*/
Client.prototype.has = function (key, fn) {
fn(null, key in this.data);
};
/**
* Deletes a key
*
* @api public
*/
Client.prototype.del = function (key, fn) {
delete this.data[key];
fn && fn(null);
return this;
};
/**
* Destroys the client.
*
* @param {Number} number of seconds to expire data
* @api private
*/
Client.prototype.destroy = function (expiration) {
if ('number' != typeof expiration) {
this.data = {};
} else {
var self = this;
setTimeout(function () {
self.data = {};
}, expiration * 1000);
}
return this;
};

269
lib/stores/redis.js Normal file
View File

@@ -0,0 +1,269 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var crypto = require('crypto')
, Store = require('../store')
, assert = require('assert');
/**
* Exports the constructor.
*/
exports = module.exports = Redis;
Redis.Client = Client;
/**
* Redis store.
* Options:
* - nodeId (fn) gets an id that uniquely identifies this node
* - redis (fn) redis constructor, defaults to redis
* - redisPub (object) options to pass to the pub redis client
* - redisSub (object) options to pass to the sub redis client
* - redisClient (object) options to pass to the general redis client
* - pack (fn) custom packing, defaults to JSON or msgpack if installed
* - unpack (fn) custom packing, defaults to JSON or msgpack if installed
*
* @api public
*/
function Redis (opts) {
opts = opts || {};
// node id to uniquely identify this node
var nodeId = opts.nodeId || function () {
// by default, we generate a random id
return Math.abs(Math.random() * Math.random() * Date.now() | 0);
};
this.nodeId = nodeId();
// packing / unpacking mechanism
if (opts.pack) {
this.pack = opts.pack;
this.unpack = opts.unpack;
} else {
try {
var msgpack = require('msgpack');
this.pack = msgpack.pack;
this.unpack = msgpack.unpack;
} catch (e) {
this.pack = JSON.stringify;
this.unpack = JSON.parse;
}
}
var redis = opts.redis || require('redis')
, RedisClient = redis.RedisClient;
// initialize a pubsub client and a regular client
if (opts.redisPub instanceof RedisClient) {
this.pub = opts.redisPub;
} else {
opts.redisPub || (opts.redisPub = {});
this.pub = redis.createClient(opts.redisPub.port, opts.redisPub.host, opts.redisPub);
}
if (opts.redisSub instanceof RedisClient) {
this.sub = opts.redisSub;
} else {
opts.redisSub || (opts.redisSub = {});
this.sub = redis.createClient(opts.redisSub.port, opts.redisSub.host, opts.redisSub);
}
if (opts.redisClient instanceof RedisClient) {
this.cmd = opts.redisClient;
} else {
opts.redisClient || (opts.redisClient = {});
this.cmd = redis.createClient(opts.redisClient.port, opts.redisClient.host, opts.redisClient);
}
Store.call(this, opts);
this.sub.setMaxListeners(0);
this.setMaxListeners(0);
};
/**
* Inherits from Store.
*/
Redis.prototype.__proto__ = Store.prototype;
/**
* Publishes a message.
*
* @api private
*/
Redis.prototype.publish = function (name) {
var args = Array.prototype.slice.call(arguments, 1);
this.pub.publish(name, this.pack({ nodeId: this.nodeId, args: args }));
this.emit.apply(this, ['publish', name].concat(args));
};
/**
* Subscribes to a channel
*
* @api private
*/
Redis.prototype.subscribe = function (name, consumer, fn) {
this.sub.subscribe(name);
if (consumer || fn) {
var self = this;
self.sub.on('subscribe', function subscribe (ch) {
if (name == ch) {
function message (ch, msg) {
if (name == ch) {
msg = self.unpack(msg);
// we check that the message consumed wasnt emitted by this node
if (self.nodeId != msg.nodeId) {
consumer.apply(null, msg.args);
}
}
};
self.sub.on('message', message);
self.on('unsubscribe', function unsubscribe (ch) {
if (name == ch) {
self.sub.removeListener('message', message);
self.removeListener('unsubscribe', unsubscribe);
}
});
self.sub.removeListener('subscribe', subscribe);
fn && fn();
}
});
}
this.emit('subscribe', name, consumer, fn);
};
/**
* Unsubscribes
*
* @api private
*/
Redis.prototype.unsubscribe = function (name, fn) {
this.sub.unsubscribe(name);
if (fn) {
var client = this.sub;
client.on('unsubscribe', function unsubscribe (ch) {
if (name == ch) {
fn();
client.removeListener('unsubscribe', unsubscribe);
}
});
}
this.emit('unsubscribe', name, fn);
};
/**
* Destroys the store
*
* @api public
*/
Redis.prototype.destroy = function () {
Store.prototype.destroy.call(this);
this.pub.end();
this.sub.end();
this.cmd.end();
};
/**
* Client constructor
*
* @api private
*/
function Client (store, id) {
Store.Client.call(this, store, id);
};
/**
* Inherits from Store.Client
*/
Client.prototype.__proto__ = Store.Client;
/**
* Redis hash get
*
* @api private
*/
Client.prototype.get = function (key, fn) {
this.store.cmd.hget(this.id, key, fn);
return this;
};
/**
* Redis hash set
*
* @api private
*/
Client.prototype.set = function (key, value, fn) {
this.store.cmd.hset(this.id, key, value, fn);
return this;
};
/**
* Redis hash del
*
* @api private
*/
Client.prototype.del = function (key, fn) {
this.store.cmd.hdel(this.id, key, fn);
return this;
};
/**
* Redis hash has
*
* @api private
*/
Client.prototype.has = function (key, fn) {
this.store.cmd.hexists(this.id, key, function (err, has) {
if (err) return fn(err);
fn(null, !!has);
});
return this;
};
/**
* Destroys client
*
* @param {Number} number of seconds to expire data
* @api private
*/
Client.prototype.destroy = function (expiration) {
if ('number' != typeof expiration) {
this.store.cmd.del(this.id);
} else {
this.store.cmd.expire(this.id, expiration);
}
return this;
};

534
lib/transport.js Normal file
View File

@@ -0,0 +1,534 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var parser = require('./parser');
/**
* Expose the constructor.
*/
exports = module.exports = Transport;
/**
* Transport constructor.
*
* @api public
*/
function Transport (mng, data, req) {
this.manager = mng;
this.id = data.id;
this.disconnected = false;
this.drained = true;
this.handleRequest(req);
};
/**
* Access the logger.
*
* @api public
*/
Transport.prototype.__defineGetter__('log', function () {
return this.manager.log;
});
/**
* Access the store.
*
* @api public
*/
Transport.prototype.__defineGetter__('store', function () {
return this.manager.store;
});
/**
* Handles a request when it's set.
*
* @api private
*/
Transport.prototype.handleRequest = function (req) {
this.log.debug('setting request', req.method, req.url);
this.req = req;
if (req.method == 'GET') {
this.socket = req.socket;
this.open = true;
this.drained = true;
this.setHeartbeatInterval();
this.setHandlers();
this.onSocketConnect();
}
};
/**
* Called when a connection is first set.
*
* @api private
*/
Transport.prototype.onSocketConnect = function () { };
/**
* Sets transport handlers
*
* @api private
*/
Transport.prototype.setHandlers = function () {
var self = this;
// we need to do this in a pub/sub way since the client can POST the message
// over a different socket (ie: different Transport instance)
this.store.subscribe('heartbeat-clear:' + this.id, function () {
self.onHeartbeatClear();
});
this.store.subscribe('disconnect-force:' + this.id, function () {
self.onForcedDisconnect();
});
this.store.subscribe('dispatch:' + this.id, function (packet, volatile) {
self.onDispatch(packet, volatile);
});
this.bound = {
end: this.onSocketEnd.bind(this)
, close: this.onSocketClose.bind(this)
, error: this.onSocketError.bind(this)
, drain: this.onSocketDrain.bind(this)
};
this.socket.on('end', this.bound.end);
this.socket.on('close', this.bound.close);
this.socket.on('error', this.bound.error);
this.socket.on('drain', this.bound.drain);
this.handlersSet = true;
};
/**
* Removes transport handlers
*
* @api private
*/
Transport.prototype.clearHandlers = function () {
if (this.handlersSet) {
this.store.unsubscribe('disconnect-force:' + this.id);
this.store.unsubscribe('heartbeat-clear:' + this.id);
this.store.unsubscribe('dispatch:' + this.id);
this.socket.removeListener('end', this.bound.end);
this.socket.removeListener('close', this.bound.close);
this.socket.removeListener('error', this.bound.error);
this.socket.removeListener('drain', this.bound.drain);
}
};
/**
* Called when the connection dies
*
* @api private
*/
Transport.prototype.onSocketEnd = function () {
this.end('socket end');
};
/**
* Called when the connection dies
*
* @api private
*/
Transport.prototype.onSocketClose = function (error) {
this.end(error ? 'socket error' : 'socket close');
};
/**
* Called when the connection has an error.
*
* @api private
*/
Transport.prototype.onSocketError = function (err) {
if (this.open) {
this.socket.destroy();
this.onClose();
}
this.log.info('socket error ' + err.stack);
};
/**
* Called when the connection is drained.
*
* @api private
*/
Transport.prototype.onSocketDrain = function () {
this.drained = true;
};
/**
* Called upon receiving a heartbeat packet.
*
* @api private
*/
Transport.prototype.onHeartbeatClear = function () {
this.clearHeartbeatTimeout();
this.setHeartbeatInterval();
};
/**
* Called upon a forced disconnection.
*
* @api private
*/
Transport.prototype.onForcedDisconnect = function () {
if (!this.disconnected) {
this.log.info('transport end by forced client disconnection');
if (this.open) {
this.packet({ type: 'disconnect' });
}
this.end('booted');
}
};
/**
* Dispatches a packet.
*
* @api private
*/
Transport.prototype.onDispatch = function (packet, volatile) {
if (volatile) {
this.writeVolatile(packet);
} else {
this.write(packet);
}
};
/**
* Sets the close timeout.
*/
Transport.prototype.setCloseTimeout = function () {
if (!this.closeTimeout) {
var self = this;
this.closeTimeout = setTimeout(function () {
self.log.debug('fired close timeout for client', self.id);
self.closeTimeout = null;
self.end('close timeout');
}, this.manager.get('close timeout') * 1000);
this.log.debug('set close timeout for client', this.id);
}
};
/**
* Clears the close timeout.
*/
Transport.prototype.clearCloseTimeout = function () {
if (this.closeTimeout) {
clearTimeout(this.closeTimeout);
this.closeTimeout = null;
this.log.debug('cleared close timeout for client', this.id);
}
};
/**
* Sets the heartbeat timeout
*/
Transport.prototype.setHeartbeatTimeout = function () {
if (!this.heartbeatTimeout && this.manager.enabled('heartbeats')) {
var self = this;
this.heartbeatTimeout = setTimeout(function () {
self.log.debug('fired heartbeat timeout for client', self.id);
self.heartbeatTimeout = null;
self.end('heartbeat timeout');
}, this.manager.get('heartbeat timeout') * 1000);
this.log.debug('set heartbeat timeout for client', this.id);
}
};
/**
* Clears the heartbeat timeout
*
* @param text
*/
Transport.prototype.clearHeartbeatTimeout = function () {
if (this.heartbeatTimeout && this.manager.enabled('heartbeats')) {
clearTimeout(this.heartbeatTimeout);
this.heartbeatTimeout = null;
this.log.debug('cleared heartbeat timeout for client', this.id);
}
};
/**
* Sets the heartbeat interval. To be called when a connection opens and when
* a heartbeat is received.
*
* @api private
*/
Transport.prototype.setHeartbeatInterval = function () {
if (!this.heartbeatInterval && this.manager.enabled('heartbeats')) {
var self = this;
this.heartbeatInterval = setTimeout(function () {
self.heartbeat();
self.heartbeatInterval = null;
}, this.manager.get('heartbeat interval') * 1000);
this.log.debug('set heartbeat interval for client', this.id);
}
};
/**
* Clears all timeouts.
*
* @api private
*/
Transport.prototype.clearTimeouts = function () {
this.clearCloseTimeout();
this.clearHeartbeatTimeout();
this.clearHeartbeatInterval();
};
/**
* Sends a heartbeat
*
* @api private
*/
Transport.prototype.heartbeat = function () {
if (this.open) {
this.log.debug('emitting heartbeat for client', this.id);
this.packet({ type: 'heartbeat' });
this.setHeartbeatTimeout();
}
return this;
};
/**
* Handles a message.
*
* @param {Object} packet object
* @api private
*/
Transport.prototype.onMessage = function (packet) {
var current = this.manager.transports[this.id];
if ('heartbeat' == packet.type) {
this.log.debug('got heartbeat packet');
if (current && current.open) {
current.onHeartbeatClear();
} else {
this.store.publish('heartbeat-clear:' + this.id);
}
} else {
if ('disconnect' == packet.type && packet.endpoint == '') {
this.log.debug('got disconnection packet');
if (current) {
current.onForcedDisconnect();
} else {
this.store.publish('disconnect-force:' + this.id);
}
return;
}
if (packet.id && packet.ack != 'data') {
this.log.debug('acknowledging packet automatically');
var ack = parser.encodePacket({
type: 'ack'
, ackId: packet.id
, endpoint: packet.endpoint || ''
});
if (current && current.open) {
current.onDispatch(ack);
} else {
this.manager.onClientDispatch(this.id, ack);
this.store.publish('dispatch:' + this.id, ack);
}
}
// handle packet locally or publish it
if (current) {
this.manager.onClientMessage(this.id, packet);
} else {
this.store.publish('message:' + this.id, packet);
}
}
};
/**
* Clears the heartbeat interval
*
* @api private
*/
Transport.prototype.clearHeartbeatInterval = function () {
if (this.heartbeatInterval && this.manager.enabled('heartbeats')) {
clearTimeout(this.heartbeatInterval);
this.heartbeatInterval = null;
this.log.debug('cleared heartbeat interval for client', this.id);
}
};
/**
* Finishes the connection and makes sure client doesn't reopen
*
* @api private
*/
Transport.prototype.disconnect = function (reason) {
this.packet({ type: 'disconnect' });
this.end(reason);
return this;
};
/**
* Closes the connection.
*
* @api private
*/
Transport.prototype.close = function () {
if (this.open) {
this.doClose();
this.onClose();
}
};
/**
* Called upon a connection close.
*
* @api private
*/
Transport.prototype.onClose = function () {
if (this.open) {
this.setCloseTimeout();
this.clearHandlers();
this.open = false;
this.manager.onClose(this.id);
this.store.publish('close', this.id);
}
};
/**
* Cleans up the connection, considers the client disconnected.
*
* @api private
*/
Transport.prototype.end = function (reason) {
if (!this.disconnected) {
this.log.info('transport end (' + reason + ')');
var local = this.manager.transports[this.id];
this.close();
this.clearTimeouts();
this.disconnected = true;
if (local) {
this.manager.onClientDisconnect(this.id, reason, true);
} else {
this.store.publish('disconnect:' + this.id, reason);
}
}
};
/**
* Signals that the transport should pause and buffer data.
*
* @api public
*/
Transport.prototype.discard = function () {
this.log.debug('discarding transport');
this.discarded = true;
this.clearTimeouts();
this.clearHandlers();
return this;
};
/**
* Writes an error packet with the specified reason and advice.
*
* @param {Number} advice
* @param {Number} reason
* @api public
*/
Transport.prototype.error = function (reason, advice) {
this.packet({
type: 'error'
, reason: reason
, advice: advice
});
this.log.warn(reason, advice ? ('client should ' + advice) : '');
this.end('error');
};
/**
* Write a packet.
*
* @api public
*/
Transport.prototype.packet = function (obj) {
return this.write(parser.encodePacket(obj));
};
/**
* Writes a volatile message.
*
* @api private
*/
Transport.prototype.writeVolatile = function (msg) {
if (this.open) {
if (this.drained) {
this.write(msg);
} else {
this.log.debug('ignoring volatile packet, buffer not drained');
}
} else {
this.log.debug('ignoring volatile packet, transport not open');
}
};

View File

@@ -0,0 +1,129 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module requirements.
*/
var WebSocket = require('./websocket');
/**
* Export the constructor.
*/
exports = module.exports = FlashSocket;
/**
* The FlashSocket transport is just a proxy
* for WebSocket connections.
*
* @api public
*/
function FlashSocket (mng, data, req) {
return WebSocket.call(this, mng, data, req);
}
/**
* Inherits from WebSocket.
*/
FlashSocket.prototype.__proto__ = WebSocket.prototype;
/**
* Transport name
*
* @api public
*/
FlashSocket.prototype.name = 'flashsocket';
/**
* Listens for new configuration changes of the Manager
* this way we can enable and disable the flash server.
*
* @param {Manager} Manager instance.
* @api private
*/
FlashSocket.init = function (manager) {
var server;
function create () {
// Drop out immediately if the user has
// disabled the flash policy server
if (!manager.get('flash policy server')) {
return;
}
server = require('policyfile').createServer({
log: function(msg){
manager.log.info(msg);
}
}, manager.get('origins'));
server.on('close', function (e) {
server = null;
});
server.listen(manager.get('flash policy port'), manager.server);
manager.flashPolicyServer = server;
}
// listen for origin changes, so we can update the server
manager.on('set:origins', function (value, key) {
if (!server) return;
// update the origins and compile a new response buffer
server.origins = Array.isArray(value) ? value : [value];
server.compile();
});
// destory the server and create a new server
manager.on('set:flash policy port', function (value, key) {
var transports = manager.get('transports');
if (~transports.indexOf('flashsocket')) {
if (server) {
if (server.port === value) return;
// destroy the server and rebuild it on a new port
try {
server.close();
}
catch (e) { /* ignore exception. could e.g. be that the server isn't started yet */ }
}
create();
}
});
// create or destroy the server
manager.on('set:flash policy server', function (value, key) {
var transports = manager.get('transports');
if (~transports.indexOf('flashsocket')) {
if (server && !value) {
// destroy the server
try {
server.close();
}
catch (e) { /* ignore exception. could e.g. be that the server isn't started yet */ }
}
} else if (!server && value) {
// create the server
create();
}
});
// only start the server
manager.on('set:transports', function (value, key){
if (!server && ~manager.get('transports').indexOf('flashsocket')) {
create();
}
});
// check if we need to initialize at start
if (~manager.get('transports').indexOf('flashsocket')){
create();
}
};

View File

@@ -0,0 +1,83 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module requirements.
*/
var HTTPTransport = require('./http');
/**
* Export the constructor.
*/
exports = module.exports = HTMLFile;
/**
* HTMLFile transport constructor.
*
* @api public
*/
function HTMLFile (mng, data, req) {
HTTPTransport.call(this, mng, data, req);
};
/**
* Inherits from Transport.
*/
HTMLFile.prototype.__proto__ = HTTPTransport.prototype;
/**
* Transport name
*
* @api public
*/
HTMLFile.prototype.name = 'htmlfile';
/**
* Handles the request.
*
* @api private
*/
HTMLFile.prototype.handleRequest = function (req) {
HTTPTransport.prototype.handleRequest.call(this, req);
if (req.method == 'GET') {
req.res.writeHead(200, {
'Content-Type': 'text/html; charset=UTF-8'
, 'Connection': 'keep-alive'
, 'Transfer-Encoding': 'chunked'
});
req.res.write(
'<html><body>'
+ '<script>var _ = function (msg) { parent.s._(msg, document); };</script>'
+ new Array(174).join(' ')
);
}
};
/**
* Performs the write.
*
* @api private
*/
HTMLFile.prototype.write = function (data) {
// escape all forward slashes. see GH-1251
data = '<script>_(' + JSON.stringify(data).replace(/\//g, '//') + ');</script>';
if (this.response.write(data)) {
this.drained = true;
}
this.log.debug(this.name + ' writing', data);
};

View File

@@ -0,0 +1,147 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module requirements.
*/
var HTTPTransport = require('./http');
/**
* Exports the constructor.
*/
exports = module.exports = HTTPPolling;
/**
* HTTP polling constructor.
*
* @api public.
*/
function HTTPPolling (mng, data, req) {
HTTPTransport.call(this, mng, data, req);
};
/**
* Inherits from HTTPTransport.
*
* @api public.
*/
HTTPPolling.prototype.__proto__ = HTTPTransport.prototype;
/**
* Transport name
*
* @api public
*/
HTTPPolling.prototype.name = 'httppolling';
/**
* Override setHandlers
*
* @api private
*/
HTTPPolling.prototype.setHandlers = function () {
HTTPTransport.prototype.setHandlers.call(this);
this.socket.removeListener('end', this.bound.end);
this.socket.removeListener('close', this.bound.close);
};
/**
* Removes heartbeat timeouts for polling.
*/
HTTPPolling.prototype.setHeartbeatInterval = function () {
return this;
};
/**
* Handles a request
*
* @api private
*/
HTTPPolling.prototype.handleRequest = function (req) {
HTTPTransport.prototype.handleRequest.call(this, req);
if (req.method == 'GET') {
var self = this;
this.pollTimeout = setTimeout(function () {
self.packet({ type: 'noop' });
self.log.debug(self.name + ' closed due to exceeded duration');
}, this.manager.get('polling duration') * 1000);
this.log.debug('setting poll timeout');
}
};
/**
* Clears polling timeout
*
* @api private
*/
HTTPPolling.prototype.clearPollTimeout = function () {
if (this.pollTimeout) {
clearTimeout(this.pollTimeout);
this.pollTimeout = null;
this.log.debug('clearing poll timeout');
}
return this;
};
/**
* Override clear timeouts to clear the poll timeout
*
* @api private
*/
HTTPPolling.prototype.clearTimeouts = function () {
HTTPTransport.prototype.clearTimeouts.call(this);
this.clearPollTimeout();
};
/**
* doWrite to clear poll timeout
*
* @api private
*/
HTTPPolling.prototype.doWrite = function () {
this.clearPollTimeout();
};
/**
* Performs a write.
*
* @api private.
*/
HTTPPolling.prototype.write = function (data, close) {
this.doWrite(data);
this.response.end();
this.onClose();
};
/**
* Override end.
*
* @api private
*/
HTTPPolling.prototype.end = function (reason) {
this.clearPollTimeout();
return HTTPTransport.prototype.end.call(this, reason);
};

121
lib/transports/http.js Normal file
View File

@@ -0,0 +1,121 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module requirements.
*/
var Transport = require('../transport')
, parser = require('../parser')
, qs = require('querystring');
/**
* Export the constructor.
*/
exports = module.exports = HTTPTransport;
/**
* HTTP interface constructor. For all non-websocket transports.
*
* @api public
*/
function HTTPTransport (mng, data, req) {
Transport.call(this, mng, data, req);
};
/**
* Inherits from Transport.
*/
HTTPTransport.prototype.__proto__ = Transport.prototype;
/**
* Handles a request.
*
* @api private
*/
HTTPTransport.prototype.handleRequest = function (req) {
// Always set the response in case an error is returned to the client
this.response = req.res;
if (req.method == 'POST') {
var buffer = ''
, res = req.res
, origin = req.headers.origin
, headers = { 'Content-Length': 1, 'Content-Type': 'text/plain; charset=UTF-8' }
, self = this;
req.on('data', function (data) {
buffer += data;
if (Buffer.byteLength(buffer) >= self.manager.get('destroy buffer size')) {
buffer = '';
req.connection.destroy();
}
});
req.on('end', function () {
res.writeHead(200, headers);
res.end('1');
self.onData(self.postEncoded ? qs.parse(buffer).d : buffer);
});
// prevent memory leaks for uncompleted requests
req.on('close', function () {
buffer = '';
self.onClose();
});
if (origin) {
// https://developer.mozilla.org/En/HTTP_Access_Control
headers['Access-Control-Allow-Origin'] = origin;
headers['Access-Control-Allow-Credentials'] = 'true';
}
} else {
Transport.prototype.handleRequest.call(this, req);
}
};
/**
* Handles data payload.
*
* @api private
*/
HTTPTransport.prototype.onData = function (data) {
var messages = parser.decodePayload(data);
this.log.debug(this.name + ' received data packet', data);
for (var i = 0, l = messages.length; i < l; i++) {
this.onMessage(messages[i]);
}
};
/**
* Closes the request-response cycle
*
* @api private
*/
HTTPTransport.prototype.doClose = function () {
this.response.end();
};
/**
* Writes a payload of messages
*
* @api private
*/
HTTPTransport.prototype.payload = function (msgs) {
this.write(parser.encodePayload(msgs));
};

12
lib/transports/index.js Normal file
View File

@@ -0,0 +1,12 @@
/**
* Export transports.
*/
module.exports = {
websocket: require('./websocket')
, flashsocket: require('./flashsocket')
, htmlfile: require('./htmlfile')
, 'xhr-polling': require('./xhr-polling')
, 'jsonp-polling': require('./jsonp-polling')
};

View File

@@ -0,0 +1,97 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module requirements.
*/
var HTTPPolling = require('./http-polling');
var jsonpolling_re = /^\d+$/
/**
* Export the constructor.
*/
exports = module.exports = JSONPPolling;
/**
* JSON-P polling transport.
*
* @api public
*/
function JSONPPolling (mng, data, req) {
HTTPPolling.call(this, mng, data, req);
this.head = 'io.j[0](';
this.foot = ');';
if (data.query.i && jsonpolling_re.test(data.query.i)) {
this.head = 'io.j[' + data.query.i + '](';
}
};
/**
* Inherits from Transport.
*/
JSONPPolling.prototype.__proto__ = HTTPPolling.prototype;
/**
* Transport name
*
* @api public
*/
JSONPPolling.prototype.name = 'jsonppolling';
/**
* Make sure POST are decoded.
*/
JSONPPolling.prototype.postEncoded = true;
/**
* Handles incoming data.
* Due to a bug in \n handling by browsers, we expect a JSONified string.
*
* @api private
*/
JSONPPolling.prototype.onData = function (data) {
try {
data = JSON.parse(data);
} catch (e) {
this.error('parse', 'reconnect');
return;
}
HTTPPolling.prototype.onData.call(this, data);
};
/**
* Performs the write.
*
* @api private
*/
JSONPPolling.prototype.doWrite = function (data) {
HTTPPolling.prototype.doWrite.call(this);
var data = data === undefined
? '' : this.head + JSON.stringify(data) + this.foot;
this.response.writeHead(200, {
'Content-Type': 'text/javascript; charset=UTF-8'
, 'Content-Length': Buffer.byteLength(data)
, 'Connection': 'Keep-Alive'
, 'X-XSS-Protection': '0'
});
this.response.write(data);
this.log.debug(this.name + ' writing', data);
};

View File

@@ -0,0 +1,36 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module requirements.
*/
var protocolVersions = require('./websocket/');
/**
* Export the constructor.
*/
exports = module.exports = WebSocket;
/**
* HTTP interface constructor. Interface compatible with all transports that
* depend on request-response cycles.
*
* @api public
*/
function WebSocket (mng, data, req) {
var transport
, version = req.headers['sec-websocket-version'];
if (typeof version !== 'undefined' && typeof protocolVersions[version] !== 'undefined') {
transport = new protocolVersions[version](mng, data, req);
}
else transport = new protocolVersions['default'](mng, data, req);
if (typeof this.name !== 'undefined') transport.name = this.name;
return transport;
};

View File

@@ -0,0 +1,362 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module requirements.
*/
var Transport = require('../../transport')
, EventEmitter = process.EventEmitter
, crypto = require('crypto')
, parser = require('../../parser');
/**
* Export the constructor.
*/
exports = module.exports = WebSocket;
/**
* HTTP interface constructor. Interface compatible with all transports that
* depend on request-response cycles.
*
* @api public
*/
function WebSocket (mng, data, req) {
// parser
var self = this;
this.parser = new Parser();
this.parser.on('data', function (packet) {
self.log.debug(self.name + ' received data packet', packet);
self.onMessage(parser.decodePacket(packet));
});
this.parser.on('close', function () {
self.end();
});
this.parser.on('error', function () {
self.end();
});
Transport.call(this, mng, data, req);
};
/**
* Inherits from Transport.
*/
WebSocket.prototype.__proto__ = Transport.prototype;
/**
* Transport name
*
* @api public
*/
WebSocket.prototype.name = 'websocket';
/**
* Websocket draft version
*
* @api public
*/
WebSocket.prototype.protocolVersion = 'hixie-76';
/**
* Called when the socket connects.
*
* @api private
*/
WebSocket.prototype.onSocketConnect = function () {
var self = this;
this.socket.setNoDelay(true);
this.buffer = true;
this.buffered = [];
if (this.req.headers.upgrade !== 'WebSocket') {
this.log.warn(this.name + ' connection invalid');
this.end();
return;
}
var origin = this.req.headers['origin']
, waitingForNonce = false;
if(this.manager.settings['match origin protocol']){
location = (origin.indexOf('https')>-1 ? 'wss' : 'ws') + '://' + this.req.headers.host + this.req.url;
}else if(this.socket.encrypted){
location = 'wss://' + this.req.headers.host + this.req.url;
}else{
location = 'ws://' + this.req.headers.host + this.req.url;
}
if (this.req.headers['sec-websocket-key1']) {
// If we don't have the nonce yet, wait for it (HAProxy compatibility).
if (! (this.req.head && this.req.head.length >= 8)) {
waitingForNonce = true;
}
var headers = [
'HTTP/1.1 101 WebSocket Protocol Handshake'
, 'Upgrade: WebSocket'
, 'Connection: Upgrade'
, 'Sec-WebSocket-Origin: ' + origin
, 'Sec-WebSocket-Location: ' + location
];
if (this.req.headers['sec-websocket-protocol']){
headers.push('Sec-WebSocket-Protocol: '
+ this.req.headers['sec-websocket-protocol']);
}
} else {
var headers = [
'HTTP/1.1 101 Web Socket Protocol Handshake'
, 'Upgrade: WebSocket'
, 'Connection: Upgrade'
, 'WebSocket-Origin: ' + origin
, 'WebSocket-Location: ' + location
];
}
try {
this.socket.write(headers.concat('', '').join('\r\n'));
this.socket.setTimeout(0);
this.socket.setNoDelay(true);
this.socket.setEncoding('utf8');
} catch (e) {
this.end();
return;
}
if (waitingForNonce) {
this.socket.setEncoding('binary');
} else if (this.proveReception(headers)) {
self.flush();
}
var headBuffer = '';
this.socket.on('data', function (data) {
if (waitingForNonce) {
headBuffer += data;
if (headBuffer.length < 8) {
return;
}
// Restore the connection to utf8 encoding after receiving the nonce
self.socket.setEncoding('utf8');
waitingForNonce = false;
// Stuff the nonce into the location where it's expected to be
self.req.head = headBuffer.substr(0, 8);
headBuffer = '';
if (self.proveReception(headers)) {
self.flush();
}
return;
}
self.parser.add(data);
});
};
/**
* Writes to the socket.
*
* @api private
*/
WebSocket.prototype.write = function (data) {
if (this.open) {
this.drained = false;
if (this.buffer) {
this.buffered.push(data);
return this;
}
var length = Buffer.byteLength(data)
, buffer = new Buffer(2 + length);
buffer.write('\x00', 'binary');
buffer.write(data, 1, 'utf8');
buffer.write('\xff', 1 + length, 'binary');
try {
if (this.socket.write(buffer)) {
this.drained = true;
}
} catch (e) {
this.end();
}
this.log.debug(this.name + ' writing', data);
}
};
/**
* Flushes the internal buffer
*
* @api private
*/
WebSocket.prototype.flush = function () {
this.buffer = false;
for (var i = 0, l = this.buffered.length; i < l; i++) {
this.write(this.buffered.splice(0, 1)[0]);
}
};
/**
* Finishes the handshake.
*
* @api private
*/
WebSocket.prototype.proveReception = function (headers) {
var self = this
, k1 = this.req.headers['sec-websocket-key1']
, k2 = this.req.headers['sec-websocket-key2'];
if (k1 && k2){
var md5 = crypto.createHash('md5');
[k1, k2].forEach(function (k) {
var n = parseInt(k.replace(/[^\d]/g, ''))
, spaces = k.replace(/[^ ]/g, '').length;
if (spaces === 0 || n % spaces !== 0){
self.log.warn('Invalid ' + self.name + ' key: "' + k + '".');
self.end();
return false;
}
n /= spaces;
md5.update(String.fromCharCode(
n >> 24 & 0xFF,
n >> 16 & 0xFF,
n >> 8 & 0xFF,
n & 0xFF));
});
md5.update(this.req.head.toString('binary'));
try {
this.socket.write(md5.digest('binary'), 'binary');
} catch (e) {
this.end();
}
}
return true;
};
/**
* Writes a payload.
*
* @api private
*/
WebSocket.prototype.payload = function (msgs) {
for (var i = 0, l = msgs.length; i < l; i++) {
this.write(msgs[i]);
}
return this;
};
/**
* Closes the connection.
*
* @api private
*/
WebSocket.prototype.doClose = function () {
this.socket.end();
};
/**
* WebSocket parser
*
* @api public
*/
function Parser () {
this.buffer = '';
this.i = 0;
};
/**
* Inherits from EventEmitter.
*/
Parser.prototype.__proto__ = EventEmitter.prototype;
/**
* Adds data to the buffer.
*
* @api public
*/
Parser.prototype.add = function (data) {
this.buffer += data;
this.parse();
};
/**
* Parses the buffer.
*
* @api private
*/
Parser.prototype.parse = function () {
for (var i = this.i, chr, l = this.buffer.length; i < l; i++){
chr = this.buffer[i];
if (this.buffer.length == 2 && this.buffer[1] == '\u0000') {
this.emit('close');
this.buffer = '';
this.i = 0;
return;
}
if (i === 0){
if (chr != '\u0000')
this.error('Bad framing. Expected null byte as first frame');
else
continue;
}
if (chr == '\ufffd'){
this.emit('data', this.buffer.substr(1, i - 1));
this.buffer = this.buffer.substr(i + 1);
this.i = 0;
return this.parse();
}
}
};
/**
* Handles an error
*
* @api private
*/
Parser.prototype.error = function (reason) {
this.buffer = '';
this.i = 0;
this.emit('error', reason);
return this;
};

View File

@@ -0,0 +1,622 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module requirements.
*/
var Transport = require('../../transport')
, EventEmitter = process.EventEmitter
, crypto = require('crypto')
, url = require('url')
, parser = require('../../parser')
, util = require('../../util');
/**
* Export the constructor.
*/
exports = module.exports = WebSocket;
exports.Parser = Parser;
/**
* HTTP interface constructor. Interface compatible with all transports that
* depend on request-response cycles.
*
* @api public
*/
function WebSocket (mng, data, req) {
// parser
var self = this;
this.manager = mng;
this.parser = new Parser();
this.parser.on('data', function (packet) {
self.onMessage(parser.decodePacket(packet));
});
this.parser.on('ping', function () {
// version 8 ping => pong
try {
self.socket.write('\u008a\u0000');
}
catch (e) {
self.end();
return;
}
});
this.parser.on('close', function () {
self.end();
});
this.parser.on('error', function (reason) {
self.log.warn(self.name + ' parser error: ' + reason);
self.end();
});
Transport.call(this, mng, data, req);
};
/**
* Inherits from Transport.
*/
WebSocket.prototype.__proto__ = Transport.prototype;
/**
* Transport name
*
* @api public
*/
WebSocket.prototype.name = 'websocket';
/**
* Websocket draft version
*
* @api public
*/
WebSocket.prototype.protocolVersion = '07-12';
/**
* Called when the socket connects.
*
* @api private
*/
WebSocket.prototype.onSocketConnect = function () {
var self = this;
if (typeof this.req.headers.upgrade === 'undefined' ||
this.req.headers.upgrade.toLowerCase() !== 'websocket') {
this.log.warn(this.name + ' connection invalid');
this.end();
return;
}
var origin = this.req.headers['sec-websocket-origin']
, location = ((this.manager.settings['match origin protocol'] ?
origin.match(/^https/) : this.socket.encrypted) ?
'wss' : 'ws')
+ '://' + this.req.headers.host + this.req.url;
if (!this.verifyOrigin(origin)) {
this.log.warn(this.name + ' connection invalid: origin mismatch');
this.end();
return;
}
if (!this.req.headers['sec-websocket-key']) {
this.log.warn(this.name + ' connection invalid: received no key');
this.end();
return;
}
// calc key
var key = this.req.headers['sec-websocket-key'];
var shasum = crypto.createHash('sha1');
shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
key = shasum.digest('base64');
var headers = [
'HTTP/1.1 101 Switching Protocols'
, 'Upgrade: websocket'
, 'Connection: Upgrade'
, 'Sec-WebSocket-Accept: ' + key
];
try {
this.socket.write(headers.concat('', '').join('\r\n'));
this.socket.setTimeout(0);
this.socket.setNoDelay(true);
} catch (e) {
this.end();
return;
}
this.socket.on('data', function (data) {
self.parser.add(data);
});
};
/**
* Verifies the origin of a request.
*
* @api private
*/
WebSocket.prototype.verifyOrigin = function (origin) {
var origins = this.manager.get('origins');
if (origin === 'null') origin = '*';
if (origins.indexOf('*:*') !== -1) {
return true;
}
if (origin) {
try {
var parts = url.parse(origin);
parts.port = parts.port || 80;
var ok =
~origins.indexOf(parts.hostname + ':' + parts.port) ||
~origins.indexOf(parts.hostname + ':*') ||
~origins.indexOf('*:' + parts.port);
if (!ok) this.log.warn('illegal origin: ' + origin);
return ok;
} catch (ex) {
this.log.warn('error parsing origin');
}
}
else {
this.log.warn('origin missing from websocket call, yet required by config');
}
return false;
};
/**
* Writes to the socket.
*
* @api private
*/
WebSocket.prototype.write = function (data) {
if (this.open) {
var buf = this.frame(0x81, data);
try {
this.socket.write(buf, 'binary');
}
catch (e) {
this.end();
return;
}
this.log.debug(this.name + ' writing', data);
}
};
/**
* Writes a payload.
*
* @api private
*/
WebSocket.prototype.payload = function (msgs) {
for (var i = 0, l = msgs.length; i < l; i++) {
this.write(msgs[i]);
}
return this;
};
/**
* Frame server-to-client output as a text packet.
*
* @api private
*/
WebSocket.prototype.frame = function (opcode, str) {
var dataBuffer = new Buffer(str)
, dataLength = dataBuffer.length
, startOffset = 2
, secondByte = dataLength;
if (dataLength > 65536) {
startOffset = 10;
secondByte = 127;
}
else if (dataLength > 125) {
startOffset = 4;
secondByte = 126;
}
var outputBuffer = new Buffer(dataLength + startOffset);
outputBuffer[0] = opcode;
outputBuffer[1] = secondByte;
dataBuffer.copy(outputBuffer, startOffset);
switch (secondByte) {
case 126:
outputBuffer[2] = dataLength >>> 8;
outputBuffer[3] = dataLength % 256;
break;
case 127:
var l = dataLength;
for (var i = 1; i <= 8; ++i) {
outputBuffer[startOffset - i] = l & 0xff;
l >>>= 8;
}
}
return outputBuffer;
};
/**
* Closes the connection.
*
* @api private
*/
WebSocket.prototype.doClose = function () {
this.socket.end();
};
/**
* WebSocket parser
*
* @api public
*/
function Parser () {
this.state = {
activeFragmentedOperation: null,
lastFragment: false,
masked: false,
opcode: 0
};
this.overflow = null;
this.expectOffset = 0;
this.expectBuffer = null;
this.expectHandler = null;
this.currentMessage = '';
var self = this;
this.opcodeHandlers = {
// text
'1': function(data) {
var finish = function(mask, data) {
self.currentMessage += self.unmask(mask, data);
if (self.state.lastFragment) {
self.emit('data', self.currentMessage);
self.currentMessage = '';
}
self.endPacket();
}
var expectData = function(length) {
if (self.state.masked) {
self.expect('Mask', 4, function(data) {
var mask = data;
self.expect('Data', length, function(data) {
finish(mask, data);
});
});
}
else {
self.expect('Data', length, function(data) {
finish(null, data);
});
}
}
// decode length
var firstLength = data[1] & 0x7f;
if (firstLength < 126) {
expectData(firstLength);
}
else if (firstLength == 126) {
self.expect('Length', 2, function(data) {
expectData(util.unpack(data));
});
}
else if (firstLength == 127) {
self.expect('Length', 8, function(data) {
if (util.unpack(data.slice(0, 4)) != 0) {
self.error('packets with length spanning more than 32 bit is currently not supported');
return;
}
var lengthBytes = data.slice(4); // note: cap to 32 bit length
expectData(util.unpack(data));
});
}
},
// binary
'2': function(data) {
var finish = function(mask, data) {
if (typeof self.currentMessage == 'string') self.currentMessage = []; // build a buffer list
self.currentMessage.push(self.unmask(mask, data, true));
if (self.state.lastFragment) {
self.emit('binary', self.concatBuffers(self.currentMessage));
self.currentMessage = '';
}
self.endPacket();
}
var expectData = function(length) {
if (self.state.masked) {
self.expect('Mask', 4, function(data) {
var mask = data;
self.expect('Data', length, function(data) {
finish(mask, data);
});
});
}
else {
self.expect('Data', length, function(data) {
finish(null, data);
});
}
}
// decode length
var firstLength = data[1] & 0x7f;
if (firstLength < 126) {
expectData(firstLength);
}
else if (firstLength == 126) {
self.expect('Length', 2, function(data) {
expectData(util.unpack(data));
});
}
else if (firstLength == 127) {
self.expect('Length', 8, function(data) {
if (util.unpack(data.slice(0, 4)) != 0) {
self.error('packets with length spanning more than 32 bit is currently not supported');
return;
}
var lengthBytes = data.slice(4); // note: cap to 32 bit length
expectData(util.unpack(data));
});
}
},
// close
'8': function(data) {
self.emit('close');
self.reset();
},
// ping
'9': function(data) {
if (self.state.lastFragment == false) {
self.error('fragmented ping is not supported');
return;
}
var finish = function(mask, data) {
self.emit('ping', self.unmask(mask, data));
self.endPacket();
}
var expectData = function(length) {
if (self.state.masked) {
self.expect('Mask', 4, function(data) {
var mask = data;
self.expect('Data', length, function(data) {
finish(mask, data);
});
});
}
else {
self.expect('Data', length, function(data) {
finish(null, data);
});
}
}
// decode length
var firstLength = data[1] & 0x7f;
if (firstLength == 0) {
finish(null, null);
}
else if (firstLength < 126) {
expectData(firstLength);
}
else if (firstLength == 126) {
self.expect('Length', 2, function(data) {
expectData(util.unpack(data));
});
}
else if (firstLength == 127) {
self.expect('Length', 8, function(data) {
expectData(util.unpack(data));
});
}
}
}
this.expect('Opcode', 2, this.processPacket);
};
/**
* Inherits from EventEmitter.
*/
Parser.prototype.__proto__ = EventEmitter.prototype;
/**
* Add new data to the parser.
*
* @api public
*/
Parser.prototype.add = function(data) {
if (this.expectBuffer == null) {
this.addToOverflow(data);
return;
}
var toRead = Math.min(data.length, this.expectBuffer.length - this.expectOffset);
data.copy(this.expectBuffer, this.expectOffset, 0, toRead);
this.expectOffset += toRead;
if (toRead < data.length) {
// at this point the overflow buffer shouldn't at all exist
this.overflow = new Buffer(data.length - toRead);
data.copy(this.overflow, 0, toRead, toRead + this.overflow.length);
}
if (this.expectOffset == this.expectBuffer.length) {
var bufferForHandler = this.expectBuffer;
this.expectBuffer = null;
this.expectOffset = 0;
this.expectHandler.call(this, bufferForHandler);
}
}
/**
* Adds a piece of data to the overflow.
*
* @api private
*/
Parser.prototype.addToOverflow = function(data) {
if (this.overflow == null) this.overflow = data;
else {
var prevOverflow = this.overflow;
this.overflow = new Buffer(this.overflow.length + data.length);
prevOverflow.copy(this.overflow, 0);
data.copy(this.overflow, prevOverflow.length);
}
}
/**
* Waits for a certain amount of bytes to be available, then fires a callback.
*
* @api private
*/
Parser.prototype.expect = function(what, length, handler) {
this.expectBuffer = new Buffer(length);
this.expectOffset = 0;
this.expectHandler = handler;
if (this.overflow != null) {
var toOverflow = this.overflow;
this.overflow = null;
this.add(toOverflow);
}
}
/**
* Start processing a new packet.
*
* @api private
*/
Parser.prototype.processPacket = function (data) {
if ((data[0] & 0x70) != 0) {
this.error('reserved fields must be empty');
}
this.state.lastFragment = (data[0] & 0x80) == 0x80;
this.state.masked = (data[1] & 0x80) == 0x80;
var opcode = data[0] & 0xf;
if (opcode == 0) {
// continuation frame
this.state.opcode = this.state.activeFragmentedOperation;
if (!(this.state.opcode == 1 || this.state.opcode == 2)) {
this.error('continuation frame cannot follow current opcode')
return;
}
}
else {
this.state.opcode = opcode;
if (this.state.lastFragment === false) {
this.state.activeFragmentedOperation = opcode;
}
}
var handler = this.opcodeHandlers[this.state.opcode];
if (typeof handler == 'undefined') this.error('no handler for opcode ' + this.state.opcode);
else handler(data);
}
/**
* Endprocessing a packet.
*
* @api private
*/
Parser.prototype.endPacket = function() {
this.expectOffset = 0;
this.expectBuffer = null;
this.expectHandler = null;
if (this.state.lastFragment && this.state.opcode == this.state.activeFragmentedOperation) {
// end current fragmented operation
this.state.activeFragmentedOperation = null;
}
this.state.lastFragment = false;
this.state.opcode = this.state.activeFragmentedOperation != null ? this.state.activeFragmentedOperation : 0;
this.state.masked = false;
this.expect('Opcode', 2, this.processPacket);
}
/**
* Reset the parser state.
*
* @api private
*/
Parser.prototype.reset = function() {
this.state = {
activeFragmentedOperation: null,
lastFragment: false,
masked: false,
opcode: 0
};
this.expectOffset = 0;
this.expectBuffer = null;
this.expectHandler = null;
this.overflow = null;
this.currentMessage = '';
}
/**
* Unmask received data.
*
* @api private
*/
Parser.prototype.unmask = function (mask, buf, binary) {
if (mask != null) {
for (var i = 0, ll = buf.length; i < ll; i++) {
buf[i] ^= mask[i % 4];
}
}
if (binary) return buf;
return buf != null ? buf.toString('utf8') : '';
}
/**
* Concatenates a list of buffers.
*
* @api private
*/
Parser.prototype.concatBuffers = function(buffers) {
var length = 0;
for (var i = 0, l = buffers.length; i < l; ++i) {
length += buffers[i].length;
}
var mergedBuffer = new Buffer(length);
var offset = 0;
for (var i = 0, l = buffers.length; i < l; ++i) {
buffers[i].copy(mergedBuffer, offset);
offset += buffers[i].length;
}
return mergedBuffer;
}
/**
* Handles an error
*
* @api private
*/
Parser.prototype.error = function (reason) {
this.reset();
this.emit('error', reason);
return this;
};

View File

@@ -0,0 +1,622 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module requirements.
*/
var Transport = require('../../transport')
, EventEmitter = process.EventEmitter
, crypto = require('crypto')
, url = require('url')
, parser = require('../../parser')
, util = require('../../util');
/**
* Export the constructor.
*/
exports = module.exports = WebSocket;
exports.Parser = Parser;
/**
* HTTP interface constructor. Interface compatible with all transports that
* depend on request-response cycles.
*
* @api public
*/
function WebSocket (mng, data, req) {
// parser
var self = this;
this.manager = mng;
this.parser = new Parser();
this.parser.on('data', function (packet) {
self.onMessage(parser.decodePacket(packet));
});
this.parser.on('ping', function () {
// version 8 ping => pong
try {
self.socket.write('\u008a\u0000');
}
catch (e) {
self.end();
return;
}
});
this.parser.on('close', function () {
self.end();
});
this.parser.on('error', function (reason) {
self.log.warn(self.name + ' parser error: ' + reason);
self.end();
});
Transport.call(this, mng, data, req);
};
/**
* Inherits from Transport.
*/
WebSocket.prototype.__proto__ = Transport.prototype;
/**
* Transport name
*
* @api public
*/
WebSocket.prototype.name = 'websocket';
/**
* Websocket draft version
*
* @api public
*/
WebSocket.prototype.protocolVersion = '16';
/**
* Called when the socket connects.
*
* @api private
*/
WebSocket.prototype.onSocketConnect = function () {
var self = this;
if (typeof this.req.headers.upgrade === 'undefined' ||
this.req.headers.upgrade.toLowerCase() !== 'websocket') {
this.log.warn(this.name + ' connection invalid');
this.end();
return;
}
var origin = this.req.headers['origin'] || ''
, location = ((this.manager.settings['match origin protocol'] ?
origin.match(/^https/) : this.socket.encrypted) ?
'wss' : 'ws')
+ '://' + this.req.headers.host + this.req.url;
if (!this.verifyOrigin(origin)) {
this.log.warn(this.name + ' connection invalid: origin mismatch');
this.end();
return;
}
if (!this.req.headers['sec-websocket-key']) {
this.log.warn(this.name + ' connection invalid: received no key');
this.end();
return;
}
// calc key
var key = this.req.headers['sec-websocket-key'];
var shasum = crypto.createHash('sha1');
shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
key = shasum.digest('base64');
var headers = [
'HTTP/1.1 101 Switching Protocols'
, 'Upgrade: websocket'
, 'Connection: Upgrade'
, 'Sec-WebSocket-Accept: ' + key
];
try {
this.socket.write(headers.concat('', '').join('\r\n'));
this.socket.setTimeout(0);
this.socket.setNoDelay(true);
} catch (e) {
this.end();
return;
}
this.socket.on('data', function (data) {
self.parser.add(data);
});
};
/**
* Verifies the origin of a request.
*
* @api private
*/
WebSocket.prototype.verifyOrigin = function (origin) {
var origins = this.manager.get('origins');
if (origin === 'null') origin = '*';
if (origins.indexOf('*:*') !== -1) {
return true;
}
if (origin) {
try {
var parts = url.parse(origin);
parts.port = parts.port || 80;
var ok =
~origins.indexOf(parts.hostname + ':' + parts.port) ||
~origins.indexOf(parts.hostname + ':*') ||
~origins.indexOf('*:' + parts.port);
if (!ok) this.log.warn('illegal origin: ' + origin);
return ok;
} catch (ex) {
this.log.warn('error parsing origin');
}
}
else {
this.log.warn('origin missing from websocket call, yet required by config');
}
return false;
};
/**
* Writes to the socket.
*
* @api private
*/
WebSocket.prototype.write = function (data) {
if (this.open) {
var buf = this.frame(0x81, data);
try {
this.socket.write(buf, 'binary');
}
catch (e) {
this.end();
return;
}
this.log.debug(this.name + ' writing', data);
}
};
/**
* Writes a payload.
*
* @api private
*/
WebSocket.prototype.payload = function (msgs) {
for (var i = 0, l = msgs.length; i < l; i++) {
this.write(msgs[i]);
}
return this;
};
/**
* Frame server-to-client output as a text packet.
*
* @api private
*/
WebSocket.prototype.frame = function (opcode, str) {
var dataBuffer = new Buffer(str)
, dataLength = dataBuffer.length
, startOffset = 2
, secondByte = dataLength;
if (dataLength > 65536) {
startOffset = 10;
secondByte = 127;
}
else if (dataLength > 125) {
startOffset = 4;
secondByte = 126;
}
var outputBuffer = new Buffer(dataLength + startOffset);
outputBuffer[0] = opcode;
outputBuffer[1] = secondByte;
dataBuffer.copy(outputBuffer, startOffset);
switch (secondByte) {
case 126:
outputBuffer[2] = dataLength >>> 8;
outputBuffer[3] = dataLength % 256;
break;
case 127:
var l = dataLength;
for (var i = 1; i <= 8; ++i) {
outputBuffer[startOffset - i] = l & 0xff;
l >>>= 8;
}
}
return outputBuffer;
};
/**
* Closes the connection.
*
* @api private
*/
WebSocket.prototype.doClose = function () {
this.socket.end();
};
/**
* WebSocket parser
*
* @api public
*/
function Parser () {
this.state = {
activeFragmentedOperation: null,
lastFragment: false,
masked: false,
opcode: 0
};
this.overflow = null;
this.expectOffset = 0;
this.expectBuffer = null;
this.expectHandler = null;
this.currentMessage = '';
var self = this;
this.opcodeHandlers = {
// text
'1': function(data) {
var finish = function(mask, data) {
self.currentMessage += self.unmask(mask, data);
if (self.state.lastFragment) {
self.emit('data', self.currentMessage);
self.currentMessage = '';
}
self.endPacket();
}
var expectData = function(length) {
if (self.state.masked) {
self.expect('Mask', 4, function(data) {
var mask = data;
self.expect('Data', length, function(data) {
finish(mask, data);
});
});
}
else {
self.expect('Data', length, function(data) {
finish(null, data);
});
}
}
// decode length
var firstLength = data[1] & 0x7f;
if (firstLength < 126) {
expectData(firstLength);
}
else if (firstLength == 126) {
self.expect('Length', 2, function(data) {
expectData(util.unpack(data));
});
}
else if (firstLength == 127) {
self.expect('Length', 8, function(data) {
if (util.unpack(data.slice(0, 4)) != 0) {
self.error('packets with length spanning more than 32 bit is currently not supported');
return;
}
var lengthBytes = data.slice(4); // note: cap to 32 bit length
expectData(util.unpack(data));
});
}
},
// binary
'2': function(data) {
var finish = function(mask, data) {
if (typeof self.currentMessage == 'string') self.currentMessage = []; // build a buffer list
self.currentMessage.push(self.unmask(mask, data, true));
if (self.state.lastFragment) {
self.emit('binary', self.concatBuffers(self.currentMessage));
self.currentMessage = '';
}
self.endPacket();
}
var expectData = function(length) {
if (self.state.masked) {
self.expect('Mask', 4, function(data) {
var mask = data;
self.expect('Data', length, function(data) {
finish(mask, data);
});
});
}
else {
self.expect('Data', length, function(data) {
finish(null, data);
});
}
}
// decode length
var firstLength = data[1] & 0x7f;
if (firstLength < 126) {
expectData(firstLength);
}
else if (firstLength == 126) {
self.expect('Length', 2, function(data) {
expectData(util.unpack(data));
});
}
else if (firstLength == 127) {
self.expect('Length', 8, function(data) {
if (util.unpack(data.slice(0, 4)) != 0) {
self.error('packets with length spanning more than 32 bit is currently not supported');
return;
}
var lengthBytes = data.slice(4); // note: cap to 32 bit length
expectData(util.unpack(data));
});
}
},
// close
'8': function(data) {
self.emit('close');
self.reset();
},
// ping
'9': function(data) {
if (self.state.lastFragment == false) {
self.error('fragmented ping is not supported');
return;
}
var finish = function(mask, data) {
self.emit('ping', self.unmask(mask, data));
self.endPacket();
}
var expectData = function(length) {
if (self.state.masked) {
self.expect('Mask', 4, function(data) {
var mask = data;
self.expect('Data', length, function(data) {
finish(mask, data);
});
});
}
else {
self.expect('Data', length, function(data) {
finish(null, data);
});
}
}
// decode length
var firstLength = data[1] & 0x7f;
if (firstLength == 0) {
finish(null, null);
}
else if (firstLength < 126) {
expectData(firstLength);
}
else if (firstLength == 126) {
self.expect('Length', 2, function(data) {
expectData(util.unpack(data));
});
}
else if (firstLength == 127) {
self.expect('Length', 8, function(data) {
expectData(util.unpack(data));
});
}
}
}
this.expect('Opcode', 2, this.processPacket);
};
/**
* Inherits from EventEmitter.
*/
Parser.prototype.__proto__ = EventEmitter.prototype;
/**
* Add new data to the parser.
*
* @api public
*/
Parser.prototype.add = function(data) {
if (this.expectBuffer == null) {
this.addToOverflow(data);
return;
}
var toRead = Math.min(data.length, this.expectBuffer.length - this.expectOffset);
data.copy(this.expectBuffer, this.expectOffset, 0, toRead);
this.expectOffset += toRead;
if (toRead < data.length) {
// at this point the overflow buffer shouldn't at all exist
this.overflow = new Buffer(data.length - toRead);
data.copy(this.overflow, 0, toRead, toRead + this.overflow.length);
}
if (this.expectOffset == this.expectBuffer.length) {
var bufferForHandler = this.expectBuffer;
this.expectBuffer = null;
this.expectOffset = 0;
this.expectHandler.call(this, bufferForHandler);
}
}
/**
* Adds a piece of data to the overflow.
*
* @api private
*/
Parser.prototype.addToOverflow = function(data) {
if (this.overflow == null) this.overflow = data;
else {
var prevOverflow = this.overflow;
this.overflow = new Buffer(this.overflow.length + data.length);
prevOverflow.copy(this.overflow, 0);
data.copy(this.overflow, prevOverflow.length);
}
}
/**
* Waits for a certain amount of bytes to be available, then fires a callback.
*
* @api private
*/
Parser.prototype.expect = function(what, length, handler) {
this.expectBuffer = new Buffer(length);
this.expectOffset = 0;
this.expectHandler = handler;
if (this.overflow != null) {
var toOverflow = this.overflow;
this.overflow = null;
this.add(toOverflow);
}
}
/**
* Start processing a new packet.
*
* @api private
*/
Parser.prototype.processPacket = function (data) {
if ((data[0] & 0x70) != 0) {
this.error('reserved fields must be empty');
return;
}
this.state.lastFragment = (data[0] & 0x80) == 0x80;
this.state.masked = (data[1] & 0x80) == 0x80;
var opcode = data[0] & 0xf;
if (opcode == 0) {
// continuation frame
this.state.opcode = this.state.activeFragmentedOperation;
if (!(this.state.opcode == 1 || this.state.opcode == 2)) {
this.error('continuation frame cannot follow current opcode')
return;
}
}
else {
this.state.opcode = opcode;
if (this.state.lastFragment === false) {
this.state.activeFragmentedOperation = opcode;
}
}
var handler = this.opcodeHandlers[this.state.opcode];
if (typeof handler == 'undefined') this.error('no handler for opcode ' + this.state.opcode);
else handler(data);
}
/**
* Endprocessing a packet.
*
* @api private
*/
Parser.prototype.endPacket = function() {
this.expectOffset = 0;
this.expectBuffer = null;
this.expectHandler = null;
if (this.state.lastFragment && this.state.opcode == this.state.activeFragmentedOperation) {
// end current fragmented operation
this.state.activeFragmentedOperation = null;
}
this.state.lastFragment = false;
this.state.opcode = this.state.activeFragmentedOperation != null ? this.state.activeFragmentedOperation : 0;
this.state.masked = false;
this.expect('Opcode', 2, this.processPacket);
}
/**
* Reset the parser state.
*
* @api private
*/
Parser.prototype.reset = function() {
this.state = {
activeFragmentedOperation: null,
lastFragment: false,
masked: false,
opcode: 0
};
this.expectOffset = 0;
this.expectBuffer = null;
this.expectHandler = null;
this.overflow = null;
this.currentMessage = '';
}
/**
* Unmask received data.
*
* @api private
*/
Parser.prototype.unmask = function (mask, buf, binary) {
if (mask != null) {
for (var i = 0, ll = buf.length; i < ll; i++) {
buf[i] ^= mask[i % 4];
}
}
if (binary) return buf;
return buf != null ? buf.toString('utf8') : '';
}
/**
* Concatenates a list of buffers.
*
* @api private
*/
Parser.prototype.concatBuffers = function(buffers) {
var length = 0;
for (var i = 0, l = buffers.length; i < l; ++i) {
length += buffers[i].length;
}
var mergedBuffer = new Buffer(length);
var offset = 0;
for (var i = 0, l = buffers.length; i < l; ++i) {
buffers[i].copy(mergedBuffer, offset);
offset += buffers[i].length;
}
return mergedBuffer;
}
/**
* Handles an error
*
* @api private
*/
Parser.prototype.error = function (reason) {
this.reset();
this.emit('error', reason);
return this;
};

View File

@@ -0,0 +1,11 @@
/**
* Export websocket versions.
*/
module.exports = {
7: require('./hybi-07-12'),
8: require('./hybi-07-12'),
13: require('./hybi-16'),
default: require('./default')
};

View File

@@ -0,0 +1,69 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module requirements.
*/
var HTTPPolling = require('./http-polling');
/**
* Export the constructor.
*/
exports = module.exports = XHRPolling;
/**
* Ajax polling transport.
*
* @api public
*/
function XHRPolling (mng, data, req) {
HTTPPolling.call(this, mng, data, req);
};
/**
* Inherits from Transport.
*/
XHRPolling.prototype.__proto__ = HTTPPolling.prototype;
/**
* Transport name
*
* @api public
*/
XHRPolling.prototype.name = 'xhr-polling';
/**
* Frames data prior to write.
*
* @api private
*/
XHRPolling.prototype.doWrite = function (data) {
HTTPPolling.prototype.doWrite.call(this);
var origin = this.req.headers.origin
, headers = {
'Content-Type': 'text/plain; charset=UTF-8'
, 'Content-Length': data === undefined ? 0 : Buffer.byteLength(data)
, 'Connection': 'Keep-Alive'
};
if (origin) {
// https://developer.mozilla.org/En/HTTP_Access_Control
headers['Access-Control-Allow-Origin'] = origin;
headers['Access-Control-Allow-Credentials'] = 'true';
}
this.response.writeHead(200, headers);
this.response.write(data);
this.log.debug(this.name + ' writing', data);
};

50
lib/util.js Normal file
View File

@@ -0,0 +1,50 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
/**
* Converts an enumerable to an array.
*
* @api public
*/
exports.toArray = function (enu) {
var arr = [];
for (var i = 0, l = enu.length; i < l; i++)
arr.push(enu[i]);
return arr;
};
/**
* Unpacks a buffer to a number.
*
* @api public
*/
exports.unpack = function (buffer) {
var n = 0;
for (var i = 0; i < buffer.length; ++i) {
n = (i == 0) ? buffer[i] : (n * 256) + buffer[i];
}
return n;
}
/**
* Left pads a string.
*
* @api public
*/
exports.padl = function (s,n,c) {
return new Array(1 + n - s.length).join(c) + s;
}

View File

@@ -1,54 +1,38 @@
{
"name": "socket.io",
"version": "1.0.0",
"description": "node.js realtime framework server",
"keywords": [
"realtime",
"framework",
"websocket",
"tcp",
"events",
"socket",
"io"
],
"repository": {
"type": "git",
"url": "git://github.com/LearnBoost/socket.io"
},
"scripts": {
"test": "make test"
},
"dependencies": {
"engine.io": "1.2.1",
"socket.io-parser": "2.1.4",
"socket.io-client": "1.0.0",
"socket.io-adapter": "0.2.0",
"has-binary-data": "0.1.1",
"debug": "0.7.4"
},
"devDependencies": {
"mocha": "1.16.2",
"expect.js": "0.3.1",
"supertest": "0.8.2",
"superagent": "0.17.0",
"istanbul": "0.2.3"
},
"contributors": [
{
"name": "Guillermo Rauch",
"email": "rauchg@gmail.com"
},
{
"name": "Arnout Kazemier",
"email": "info@3rd-eden.com"
},
{
"name": "Vladimir Dronnikov",
"email": "dronnikov@gmail.com"
},
{
"name": "Einar Otto Stangvik",
"email": "einaros@gmail.com"
"name": "socket.io"
, "version": "0.9.15"
, "description": "Real-time apps made cross-browser & easy with a WebSocket-like API"
, "homepage": "http://socket.io"
, "keywords": ["websocket", "socket", "realtime", "socket.io", "comet", "ajax"]
, "author": "Guillermo Rauch <guillermo@learnboost.com>"
, "contributors": [
{ "name": "Guillermo Rauch", "email": "rauchg@gmail.com" }
, { "name": "Arnout Kazemier", "email": "info@3rd-eden.com" }
, { "name": "Vladimir Dronnikov", "email": "dronnikov@gmail.com" }
, { "name": "Einar Otto Stangvik", "email": "einaros@gmail.com" }
]
, "repository":{
"type": "git"
, "url": "https://github.com/LearnBoost/socket.io.git"
}
, "dependencies": {
"socket.io-client": "0.9.15"
, "policyfile": "0.0.4"
, "base64id": "0.1.0"
}
, "devDependencies": {
"expresso": "0.9.2"
, "should": "*"
, "benchmark": "0.2.2"
, "microtime": "0.1.3-1"
, "colors": "0.5.1"
}
, "optionalDependencies": {
"redis": "0.7.3"
}
, "main": "index"
, "engines": { "node": ">= 0.4.0" }
, "scripts": {
"test": "make test"
}
]
}

View File

@@ -0,0 +1,27 @@
Copyright (c) 2010, Peter Griess <pg@std.in>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of node-websocket-client nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,22 @@
# This makefile exists to help run tests.
#
# If TEST_UNIX is a non-empty value, runs tests for UNIX sockets. This
# functionality is not in node-websocket-server at the moment.
.PHONY: test
all: test test-unix
test:
for f in `ls -1 test/test-*.js | grep -v unix` ; do \
echo $$f ; \
node $$f ; \
done
test-unix:
if [[ -n "$$TEST_UNIX" ]] ; then \
for f in `ls -1 test/test-*.js | grep unix` ; do \
echo $$f ; \
node $$f ; \
done \
fi

View File

@@ -0,0 +1,41 @@
A prototype [Web Socket](http://www.whatwg.org/specs/web-socket-protocol/)
client implementation for [node.js](http://nodejs.org).
Tested with
[miksago/node-websocket-server](http://github.com/miksago/node-websocket-server)
v1.2.00.
Requires [nodejs](http://nodejs.org) 0.1.98 or later.
## Installation
Install this using `npm` as follows
npm install websocket-client
... or just dump `lib/websocket.js` in your `$NODE_PATH`.
## Usage
var sys = require('sys');
var WebSocket = require('websocket').WebSocket;
var ws = new WebSocket('ws://localhost:8000/biff', 'borf');
ws.addListener('data', function(buf) {
sys.debug('Got data: ' + sys.inspect(buf));
});
ws.onmessage = function(m) {
sys.debug('Got message: ' + m);
}
## API
This supports the `send()` and `onmessage()` APIs. The `WebSocket` object will
also emit `data` events that are node `Buffer` objects, in case you want to
work with something lower-level than strings.
## Transports
Multiple transports are supported, indicated by the scheme provided to the
`WebSocket` constructor. `ws://` is a standard TCP-based Web Socket;
`ws+unix://` allows connection to a UNIX socket at the given path.

View File

@@ -0,0 +1,12 @@
var sys = require('sys');
var WebSocket = require('../lib/websocket').WebSocket;
var ws = new WebSocket('ws+unix://' + process.argv[2], 'boffo');
ws.addListener('message', function(d) {
sys.debug('Received message: ' + d.toString('utf8'));
});
ws.addListener('open', function() {
ws.send('This is a message', 1);
});

View File

@@ -0,0 +1,10 @@
var sys = require('sys');
var WebSocket = require('../lib/websocket').WebSocket;
var ws = new WebSocket('ws://localhost:8000/biff', 'borf');
ws.addListener('data', function(buf) {
sys.debug('Got data: ' + sys.inspect(buf));
});
ws.onmessage = function(m) {
sys.debug('Got message: ' + m);
}

View File

@@ -0,0 +1,13 @@
var sys = require('sys');
var ws = require('websocket-server/ws');
var srv = ws.createServer({ debug : true});
srv.addListener('connection', function(s) {
sys.debug('Got a connection!');
s._req.socket.addListener('fd', function(fd) {
sys.debug('Got an fd: ' + fd);
});
});
srv.listen(process.argv[2]);

View File

@@ -0,0 +1,617 @@
var assert = require('assert');
var buffer = require('buffer');
var crypto = require('crypto');
var events = require('events');
var http = require('http');
var net = require('net');
var urllib = require('url');
var sys = require('util');
var FRAME_NO = 0;
var FRAME_LO = 1;
var FRAME_HI = 2;
// Values for readyState as per the W3C spec
var CONNECTING = 0;
var OPEN = 1;
var CLOSING = 2;
var CLOSED = 3;
var debugLevel = parseInt(process.env.NODE_DEBUG, 16);
var debug = (debugLevel & 0x4) ?
function() { sys.error.apply(this, arguments); } :
function() { };
// Generate a Sec-WebSocket-* value
var createSecretKey = function() {
// How many spaces will we be inserting?
var numSpaces = 1 + Math.floor(Math.random() * 12);
assert.ok(1 <= numSpaces && numSpaces <= 12);
// What is the numerical value of our key?
var keyVal = (Math.floor(
Math.random() * (4294967295 / numSpaces)
) * numSpaces);
// Our string starts with a string representation of our key
var s = keyVal.toString();
// Insert 'numChars' worth of noise in the character ranges
// [0x21, 0x2f] (14 characters) and [0x3a, 0x7e] (68 characters)
var numChars = 1 + Math.floor(Math.random() * 12);
assert.ok(1 <= numChars && numChars <= 12);
for (var i = 0; i < numChars; i++) {
var pos = Math.floor(Math.random() * s.length + 1);
var c = Math.floor(Math.random() * (14 + 68));
c = (c <= 14) ?
String.fromCharCode(c + 0x21) :
String.fromCharCode((c - 14) + 0x3a);
s = s.substring(0, pos) + c + s.substring(pos, s.length);
}
// We shoudln't have any spaces in our value until we insert them
assert.equal(s.indexOf(' '), -1);
// Insert 'numSpaces' worth of spaces
for (var i = 0; i < numSpaces; i++) {
var pos = Math.floor(Math.random() * (s.length - 1)) + 1;
s = s.substring(0, pos) + ' ' + s.substring(pos, s.length);
}
assert.notEqual(s.charAt(0), ' ');
assert.notEqual(s.charAt(s.length), ' ');
return s;
};
// Generate a challenge sequence
var createChallenge = function() {
var c = '';
for (var i = 0; i < 8; i++) {
c += String.fromCharCode(Math.floor(Math.random() * 255));
}
return c;
};
// Get the value of a secret key string
//
// This strips non-digit values and divides the result by the number of
// spaces found.
var secretKeyValue = function(sk) {
var ns = 0;
var v = 0;
for (var i = 0; i < sk.length; i++) {
var cc = sk.charCodeAt(i);
if (cc == 0x20) {
ns++;
} else if (0x30 <= cc && cc <= 0x39) {
v = v * 10 + cc - 0x30;
}
}
return Math.floor(v / ns);
}
// Get the to-be-hashed value of a secret key string
//
// This takes the result of secretKeyValue() and encodes it in a big-endian
// byte string
var secretKeyHashValue = function(sk) {
var skv = secretKeyValue(sk);
var hv = '';
hv += String.fromCharCode((skv >> 24) & 0xff);
hv += String.fromCharCode((skv >> 16) & 0xff);
hv += String.fromCharCode((skv >> 8) & 0xff);
hv += String.fromCharCode((skv >> 0) & 0xff);
return hv;
};
// Compute the secret key signature based on two secret key strings and some
// handshaking data.
var computeSecretKeySignature = function(s1, s2, hs) {
assert.equal(hs.length, 8);
var hash = crypto.createHash('md5');
hash.update(secretKeyHashValue(s1));
hash.update(secretKeyHashValue(s2));
hash.update(hs);
return hash.digest('binary');
};
// Return a hex representation of the given binary string; used for debugging
var str2hex = function(str) {
var hexChars = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f'
];
var out = '';
for (var i = 0; i < str.length; i++) {
var c = str.charCodeAt(i);
out += hexChars[(c & 0xf0) >>> 4];
out += hexChars[c & 0x0f];
out += ' ';
}
return out.trim();
};
// Get the scheme for a URL, undefined if none is found
var getUrlScheme = function(url) {
var i = url.indexOf(':');
if (i == -1) {
return undefined;
}
return url.substring(0, i);
};
// Set a constant on the given object
var setConstant = function(obj, name, value) {
Object.defineProperty(obj, name, {
get : function() {
return value;
}
});
};
// WebSocket object
//
// This is intended to conform (mostly) to http://dev.w3.org/html5/websockets/
//
// N.B. Arguments are parsed in the anonymous function at the bottom of the
// constructor.
var WebSocket = function(url, proto, opts) {
events.EventEmitter.call(this);
// Retain a reference to our object
var self = this;
// State of our end of the connection
var readyState = CONNECTING;
// Whether or not the server has sent a close handshake
var serverClosed = false;
// Our underlying net.Stream instance
var stream = undefined;
opts = opts || {
origin : 'http://www.example.com'
};
// Frame parsing functions
//
// These read data from the given buffer starting at the given offset,
// looking for the end of the current frame. If found, the current frame is
// emitted and the function returns. Only a single frame is processed at a
// time.
//
// The number of bytes read to complete a frame is returned, which the
// caller is to use to advance along its buffer. If 0 is returned, no
// completed frame bytes were found, and the caller should probably enqueue
// the buffer as a continuation of the current message. If a complete frame
// is read, the function is responsible for resting 'frameType'.
// Framing data
var frameType = FRAME_NO;
var bufs = [];
var bufsBytes = 0;
// Frame-parsing functions
var frameFuncs = [
// FRAME_NO
function(buf, off) {
if (buf[off] & 0x80) {
frameType = FRAME_HI;
} else {
frameType = FRAME_LO;
}
return 1;
},
// FRAME_LO
function(buf, off) {
debug('frame_lo(' + sys.inspect(buf) + ', ' + off + ')');
// Find the first instance of 0xff, our terminating byte
for (var i = off; i < buf.length && buf[i] != 0xff; i++)
;
// We didn't find a terminating byte
if (i >= buf.length) {
return 0;
}
// We found a terminating byte; collect all bytes into a single buffer
// and emit it
var mb = null;
if (bufs.length == 0) {
mb = buf.slice(off, i);
} else {
mb = new buffer.Buffer(bufsBytes + i);
var mbOff = 0;
bufs.forEach(function(b) {
b.copy(mb, mbOff, 0, b.length);
mbOff += b.length;
});
assert.equal(mbOff, bufsBytes);
// Don't call Buffer.copy() if we're coping 0 bytes. Rather
// than being a no-op, this will trigger a range violation on
// the destination.
if (i > 0) {
buf.copy(mb, mbOff, off, i);
}
// We consumed all of the buffers that we'd been saving; clear
// things out
bufs = [];
bufsBytes = 0;
}
process.nextTick(function() {
var b = mb;
return function() {
var m = b.toString('utf8');
self.emit('data', b);
self.emit('message', m); // wss compat
if (self.onmessage) {
self.onmessage({data: m});
}
};
}());
frameType = FRAME_NO;
return i - off + 1;
},
// FRAME_HI
function(buf, off) {
debug('frame_hi(' + sys.inspect(buf) + ', ' + off + ')');
if (buf[off] !== 0) {
throw new Error('High-byte framing not supported.');
}
serverClosed = true;
return 1;
}
];
// Handle data coming from our socket
var dataListener = function(buf) {
if (buf.length <= 0 || serverClosed) {
return;
}
debug('dataListener(' + sys.inspect(buf) + ')');
var off = 0;
var consumed = 0;
do {
if (frameType < 0 || frameFuncs.length <= frameType) {
throw new Error('Unexpected frame type: ' + frameType);
}
assert.equal(bufs.length === 0, bufsBytes === 0);
assert.ok(off < buf.length);
consumed = frameFuncs[frameType](buf, off);
off += consumed;
} while (!serverClosed && consumed > 0 && off < buf.length);
if (serverClosed) {
serverCloseHandler();
}
if (consumed == 0) {
bufs.push(buf.slice(off, buf.length));
bufsBytes += buf.length - off;
}
};
// Handle incoming file descriptors
var fdListener = function(fd) {
self.emit('fd', fd);
};
// Handle errors from any source (HTTP client, stream, etc)
var errorListener = function(e) {
process.nextTick(function() {
self.emit('wserror', e);
if (self.onerror) {
self.onerror(e);
}
});
};
// Finish the closing process; destroy the socket and tell the application
// that we've closed.
var finishClose = self.finishClose = function() {
readyState = CLOSED;
if (stream) {
stream.end();
stream.destroy();
stream = undefined;
}
process.nextTick(function() {
self.emit('close');
if (self.onclose) {
self.onclose();
}
});
};
// Send a close frame to the server
var sendClose = function() {
assert.equal(OPEN, readyState);
readyState = CLOSING;
stream.write('\xff\x00', 'binary');
};
// Handle a close packet sent from the server
var serverCloseHandler = function() {
assert.ok(serverClosed);
assert.ok(readyState === OPEN || readyState === CLOSING);
bufs = [];
bufsBytes = 0;
// Handle state transitions asynchronously so that we don't change
// readyState before the application has had a chance to process data
// events which are already in the delivery pipeline. For example, a
// 'data' event could be delivered with a readyState of CLOSING if we
// received both frames in the same packet.
process.nextTick(function() {
if (readyState === OPEN) {
sendClose();
}
finishClose();
});
};
// External API
self.close = function(timeout) {
if (readyState === CONNECTING) {
// If we're still in the process of connecting, the server is not
// in a position to understand our close frame. Just nuke the
// connection and call it a day.
finishClose();
} else if (readyState === OPEN) {
sendClose();
if (timeout) {
setTimeout(finishClose, timeout * 1000);
}
}
};
self.send = function(str, fd) {
if (readyState != OPEN) {
return;
}
stream.write('\x00', 'binary');
stream.write(str, 'utf8', fd);
stream.write('\xff', 'binary');
};
// wss compat
self.write = self.send;
setConstant(self, 'url', url);
Object.defineProperty(self, 'readyState', {
get : function() {
return readyState;
}
});
// Connect and perform handshaking with the server
(function() {
// Parse constructor arguments
if (!url) {
throw new Error('Url and must be specified.');
}
// Secrets used for handshaking
var key1 = createSecretKey();
var key2 = createSecretKey();
var challenge = createChallenge();
debug(
'key1=\'' + str2hex(key1) + '\'; ' +
'key2=\'' + str2hex(key2) + '\'; ' +
'challenge=\'' + str2hex(challenge) + '\''
);
var httpHeaders = {
'Connection' : 'Upgrade',
'Upgrade' : 'WebSocket',
'Sec-WebSocket-Key1' : key1,
'Sec-WebSocket-Key2' : key2
};
if (opts.origin) {
httpHeaders['Origin'] = opts.origin;
}
if (proto) {
httpHeaders['Sec-WebSocket-Protocol'] = proto;
}
var httpPath = '/';
// Create the HTTP client that we'll use for handshaking. We'll cannabalize
// its socket via the 'upgrade' event and leave it to rot.
//
// N.B. The ws+unix:// scheme makes use of the implementation detail
// that http.Client passes its constructor arguments through,
// un-inspected to net.Stream.connect(). The latter accepts a
// string as its first argument to connect to a UNIX socket.
var opt = {};
var agent = null;
switch (getUrlScheme(url)) {
case 'ws':
var u = urllib.parse(url);
agent = new http.Agent({
host: u.hostname,
port: u.port || 80
});
opt.agent = agent;
opt.host = u.hostname;
opt.port = u.port || 80;
opt.path = (u.pathname || '/') + (u.search || '');
opt.headers = httpHeaders;
break;
case 'ws+unix':
var sockPath = url.substring('ws+unix://'.length, url.length);
var u = urllib.parse(url);
agent = new http.Agent({
host: 'localhost',
port: sockPath
});
opt.agent = agent;
opt.host = 'localhost';
opt.path = sockPath;
opt.headers = httpHeaders;
break;
default:
throw new Error('Invalid URL scheme \'' + urlScheme + '\' specified.');
}
var httpReq = http.request(opt, function() { });
var upgradeHandler = (function() {
var data = undefined;
return function(req, s, head) {
req.socket.setNoDelay(true);
stream = s;
if (readyState == CLOSED) {
stream.end();
stream.destroy();
stream = undefined;
return;
}
stream.on('data', function(d) {
if (d.length <= 0) {
return;
}
if (!data) {
data = d;
} else {
var data2 = new buffer.Buffer(data.length + d.length);
data.copy(data2, 0, 0, data.length);
d.copy(data2, data.length, 0, d.length);
data = data2;
}
if (data.length >= 16) {
var expected = computeSecretKeySignature(key1, key2, challenge);
var actual = data.slice(0, 16).toString('binary');
// Handshaking fails; we're donezo
if (actual != expected) {
debug(
'expected=\'' + str2hex(expected) + '\'; ' +
'actual=\'' + str2hex(actual) + '\''
);
process.nextTick(function() {
// N.B. Emit 'wserror' here, as 'error' is a reserved word in the
// EventEmitter world, and gets thrown.
self.emit(
'wserror',
new Error('Invalid handshake from server:' +
'expected \'' + str2hex(expected) + '\', ' +
'actual \'' + str2hex(actual) + '\''
)
);
if (self.onerror) {
self.onerror();
}
finishClose();
});
}
// Un-register our data handler and add the one to be used
// for the normal, non-handshaking case. If we have extra
// data left over, manually fire off the handler on
// whatever remains.
//
// XXX: This is lame. We should only remove the listeners
// that we added.
httpReq.removeAllListeners('upgrade');
stream.removeAllListeners('data');
stream.on('data', dataListener);
readyState = OPEN;
process.nextTick(function() {
self.emit('open');
if (self.onopen) {
self.onopen();
}
});
// Consume any leftover data
if (data.length > 16) {
stream.emit('data', data.slice(16, data.length));
}
}
});
stream.on('fd', fdListener);
stream.on('error', errorListener);
stream.on('close', function() {
errorListener(new Error('Stream closed unexpectedly.'));
});
stream.emit('data', head);
};
})();
agent.on('upgrade', upgradeHandler); // node v0.4
httpReq.on('upgrade', upgradeHandler); // node v0.5+
httpReq.write(challenge, 'binary');
httpReq.end();
})();
};
sys.inherits(WebSocket, events.EventEmitter);
exports.WebSocket = WebSocket;
// Add some constants to the WebSocket object
setConstant(WebSocket.prototype, 'CONNECTING', CONNECTING);
setConstant(WebSocket.prototype, 'OPEN', OPEN);
setConstant(WebSocket.prototype, 'CLOSING', CLOSING);
setConstant(WebSocket.prototype, 'CLOSED', CLOSED);
// vim:ts=4 sw=4 et

View File

@@ -0,0 +1,22 @@
{
"name" : "websocket-client",
"version" : "1.0.0",
"description" : "An HTML5 Web Sockets client",
"author" : "Peter Griess <pg@std.in>",
"engines" : {
"node" : ">=0.1.98"
},
"repositories" : [
{
"type" : "git",
"url" : "http://github.com/pgriess/node-websocket-client.git"
}
],
"licenses" : [
{
"type" : "BSD",
"url" : "http://github.com/pgriess/node-websocket-client/blob/master/LICENSE"
}
],
"main" : "./lib/websocket"
}

View File

@@ -0,0 +1,68 @@
// Verify that we can connect to a WebSocket server, exchange messages, and
// shut down cleanly.
var assert = require('assert');
var WebSocket = require('../lib/websocket').WebSocket;
var WebSocketServer = require('websocket-server/ws/server').Server;
var PORT = 1024 + Math.floor(Math.random() * 4096);
var C_MSG = 'Client test: ' + (Math.random() * 100);
var S_MSG = 'Server test: ' + (Math.random() * 100);
var serverGotConnection = false;
var clientGotOpen = false;
var clientGotData = false;
var clientGotMessage = false;
var serverGotMessage = false;
var serverGotClose = false;
var clientGotClose = false;
var wss = new WebSocketServer();
wss.listen(PORT, 'localhost');
wss.on('connection', function(c) {
serverGotConnection = true;
c.on('message', function(m) {
assert.equal(m, C_MSG);
serverGotMessage = true;
c.close();
});
c.on('close', function() {
serverGotClose = true;
wss.close();
});
c.write(S_MSG);
});
var ws = new WebSocket('ws://localhost:' + PORT + '/', 'biff');
ws.on('open', function() {
clientGotOpen = true;
});
ws.on('data', function(buf) {
assert.equal(typeof buf, 'object');
assert.equal(buf.toString('utf8'), S_MSG);
clientGotData = true;
ws.send(C_MSG);
});
ws.onmessage = function(m) {
assert.deepEqual(m, {data : S_MSG});
clientGotMessage = true;
};
ws.onclose = function() {
clientGotClose = true;
};
process.on('exit', function() {
assert.ok(serverGotConnection);
assert.ok(clientGotOpen);
assert.ok(clientGotData);
assert.ok(clientGotMessage);
assert.ok(serverGotMessage);
assert.ok(serverGotClose);
assert.ok(clientGotClose);
});

View File

@@ -0,0 +1,43 @@
// Verify that a connection can be closed gracefully from the client.
var assert = require('assert');
var WebSocket = require('../lib/websocket').WebSocket;
var WebSocketServer = require('websocket-server/ws/server').Server;
var PORT = 1024 + Math.floor(Math.random() * 4096);
var C_MSG = 'Client test: ' + (Math.random() * 100);
var serverGotClientMessage = false;
var clientGotServerClose = false;
var serverGotClientClose = false;
var wss = new WebSocketServer();
wss.listen(PORT, 'localhost');
wss.on('connection', function(c) {
c.on('message', function(m) {
assert.equal(m, C_MSG);
serverGotClientMessage = true;
});
c.on('close', function() {
serverGotClientClose = true;
wss.close();
});
});
var ws = new WebSocket('ws://localhost:' + PORT);
ws.onopen = function() {
ws.send(C_MSG);
// XXX: Add a timeout here
ws.close(5);
};
ws.onclose = function() {
assert.equal(ws.CLOSED, ws.readyState);
clientGotServerClose = true;
};
process.on('exit', function() {
assert.ok(serverGotClientMessage);
assert.ok(clientGotServerClose);
assert.ok(serverGotClientClose);
});

View File

@@ -0,0 +1,43 @@
// Verify that some attributes of a WebSocket object are read-only.
var assert = require('assert');
var sys = require('sys');
var WebSocket = require('../lib/websocket').WebSocket;
var WebSocketServer = require('websocket-server/ws/server').Server;
var PORT = 1024 + Math.floor(Math.random() * 4096);
var wss = new WebSocketServer();
wss.listen(PORT, 'localhost');
wss.on('connection', function(c) {
c.close();
wss.close();
});
var ws = new WebSocket('ws://localhost:' + PORT + '/', 'biff');
ws.on('open', function() {
assert.equal(ws.CONNECTING, 0);
try {
ws.CONNECTING = 13;
assert.equal(
ws.CONNECTING, 0,
'Should not have been able to set read-only CONNECTING attribute'
);
} catch (e) {
assert.equal(e.type, 'no_setter_in_callback');
}
assert.equal(ws.OPEN, 1);
assert.equal(ws.CLOSING, 2);
assert.equal(ws.CLOSED, 3);
assert.equal(ws.url, 'ws://localhost:' + PORT + '/');
try {
ws.url = 'foobar';
assert.equal(
ws.url, 'ws://localhost:' + PORT + '/',
'Should not have been able to set read-only url attribute'
);
} catch (e) {
assert.equal(e.type, 'no_setter_in_callback');
}
});

View File

@@ -0,0 +1,26 @@
// Verify that readyState transitions are implemented correctly
var assert = require('assert');
var WebSocket = require('../lib/websocket').WebSocket;
var WebSocketServer = require('websocket-server/ws/server').Server;
var PORT = 1024 + Math.floor(Math.random() * 4096);
var wss = new WebSocketServer();
wss.listen(PORT, 'localhost');
wss.on('connection', function(c) {
c.close();
});
var ws = new WebSocket('ws://localhost:' + PORT);
assert.equal(ws.readyState, ws.CONNECTING);
ws.onopen = function() {
assert.equal(ws.readyState, ws.OPEN);
ws.close();
assert.ok(ws.readyState == ws.CLOSING);
};
ws.onclose = function() {
assert.equal(ws.readyState, ws.CLOSED);
wss.close();
};

View File

@@ -0,0 +1,41 @@
// Verify that a connection can be closed gracefully from the server.
var assert = require('assert');
var WebSocket = require('../lib/websocket').WebSocket;
var WebSocketServer = require('websocket-server/ws/server').Server;
var PORT = 1024 + Math.floor(Math.random() * 4096);
var S_MSG = 'Server test: ' + (Math.random() * 100);
var clientGotServerMessage = false;
var clientGotServerClose = false;
var serverGotClientClose = false;
var wss = new WebSocketServer();
wss.listen(PORT, 'localhost');
wss.on('connection', function(c) {
c.on('close', function() {
serverGotClientClose = true;
wss.close();
});
c.write(S_MSG);
c.close();
});
var ws = new WebSocket('ws://localhost:' + PORT);
ws.onmessage = function(m) {
assert.deepEqual(m, {data: S_MSG});
clientGotServerMessage = true;
};
ws.onclose = function() {
assert.equal(ws.CLOSED, ws.readyState);
clientGotServerClose = true;
};
process.on('exit', function() {
assert.ok(clientGotServerMessage);
assert.ok(clientGotServerClose);
assert.ok(serverGotClientClose);
});

View File

@@ -0,0 +1,63 @@
// Verify that both sides of the WS connection can both send and receive file
// descriptors.
var assert = require('assert');
var fs = require('fs');
var path = require('path');
var sys = require('sys');
var WebSocket = require('../lib/websocket').WebSocket;
var WebSocketServer = require('websocket-server/ws/server').Server;
var PATH = path.join(__dirname, 'sock.' + process.pid);
var C_MSG = 'Client test: ' + (Math.random() * 100);
var S_MSG = 'Server test: ' + (Math.random() * 100);
var clientReceivedData = false;
var clientReceivedFD = false;
var serverReceivedData = false;
var serverReceivedFD = false;
var wss = new WebSocketServer();
wss.on('listening', function() {
var ws = new WebSocket('ws+unix://' + PATH);
ws.on('data', function(d) {
assert.equal(d.toString('utf8'), S_MSG);
clientReceivedData = true;
ws.send(C_MSG, 1);
ws.close();
});
ws.on('fd', function(fd) {
assert.ok(fd >= 0);
clientReceivedFD = true;
});
});
wss.on('connection', function(c) {
c.write(S_MSG, 0);
c._req.socket.on('fd', function(fd) {
assert.ok(fd >= 0);
serverReceivedFD = true;
});
c.on('message', function(d) {
assert.equal(d, C_MSG);
serverReceivedData = true;
wss.close();
});
});
wss.listen(PATH);
process.on('exit', function() {
assert.ok(clientReceivedFD);
assert.ok(clientReceivedData);
assert.ok(serverReceivedFD);
assert.ok(serverReceivedData);
try {
fs.unlinkSync(PATH);
} catch (e) { }
});

View File

@@ -0,0 +1,46 @@
// Verify that we can connect to a server over UNIX domain sockets.
var assert = require('assert');
var fs = require('fs');
var path = require('path');
var sys = require('sys');
var WebSocket = require('../lib/websocket').WebSocket;
var WebSocketServer = require('websocket-server/ws/server').Server;
var PATH = path.join(__dirname, 'sock.' + process.pid);
var S_MSG = 'Server test: ' + (Math.random() * 100);
var serverGotConnection = false;
var clientGotOpen = false;
var clientGotData = false;
var wss = new WebSocketServer();
wss.on('listening', function() {
var ws = new WebSocket('ws+unix://' + PATH);
ws.on('open', function() {
clientGotOpen = true;
ws.close();
});
ws.on('data', function(d) {
assert.equal(d.toString('utf8'), S_MSG);
clientGotData = true;
});
});
wss.on('connection', function(c) {
serverGotConnection = true;
c.write(S_MSG);
wss.close();
});
wss.listen(PATH);
process.on('exit', function() {
assert.ok(serverGotConnection);
assert.ok(clientGotOpen);
assert.ok(clientGotData);
try {
fs.unlinkSync(PATH);
} catch(e) { }
});

278
test/common.js Normal file
View File

@@ -0,0 +1,278 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Test dependencies.
*/
var io = require('../')
, parser = io.parser
, http = require('http')
, https = require('https')
, WebSocket = require('../support/node-websocket-client/lib/websocket').WebSocket;
/**
* Exports.
*/
var should = module.exports = require('should');
should.HTTPClient = HTTPClient;
/**
* Client utility.
*
* @api publiC
*/
function HTTPClient (port) {
this.port = port;
this.agent = new http.Agent({
host: 'localhost'
, port: port
});
};
/**
* Issue a request
*
* @api private
*/
HTTPClient.prototype.request = function (path, opts, fn) {
if ('function' == typeof opts) {
fn = opts;
opts = {};
}
opts = opts || {};
opts.agent = this.agent;
opts.host = 'localhost';
opts.port = this.port;
opts.path = path.replace(/{protocol}/g, io.protocol);
opts.headers = opts.headers || {};
opts.headers.Host = 'localhost';
opts.headers.Connection = 'keep-alive';
var req = http.request(opts, function (res) {
if (false === opts.buffer)
return fn && fn(res);
var buf = '';
res.on('data', function (chunk) {
buf += chunk;
});
res.on('end', function () {
fn && fn(res, opts.parse ? opts.parse(buf) : buf);
});
});
req.on('error', function (err) { });
if (undefined !== opts.data)
req.write(opts.data);
req.end();
return req;
};
/**
* Terminates the client and associated connections.
*
* @api public
*/
HTTPClient.prototype.end = function () {
// node <v0.5 compat
if (this.agent.sockets.forEach) {
this.agent.sockets.forEach(function (socket) {
if (socket.end) socket.end();
});
return;
}
// node >=v0.5 compat
var self = this;
Object.keys(this.agent.sockets).forEach(function (socket) {
for (var i = 0, l = self.agent.sockets[socket].length; i < l; ++i) {
if (self.agent.sockets[socket][i]._handle) {
if (self.agent.sockets[socket][i]._handle.socket) {
self.agent.sockets[socket][i]._handle.socket.end();
} else {
self.agent.sockets[socket][i]._handle.owner.end();
}
}
}
});
};
/**
* Issue a GET request
*
* @api public
*/
HTTPClient.prototype.get = function (path, opts, fn) {
if ('function' == typeof opts) {
fn = opts;
opts = {};
}
opts = opts || {};
opts.method = 'GET';
// override the parser for transport requests
if (/\/(xhr-polling|htmlfile|jsonp-polling)\//.test(path)) {
// parser that might be necessary for transport-specific framing
var transportParse = opts.parse;
opts.parse = function (data) {
if (data === '') return data;
data = transportParse ? transportParse(data) : data;
return parser.decodePayload(data);
};
} else {
opts.parse = undefined;
}
return this.request(path, opts, fn);
};
/**
* Issue a POST request
*
* @api private
*/
HTTPClient.prototype.post = function (path, data, opts, fn) {
if ('function' == typeof opts) {
fn = opts;
opts = {};
}
opts = opts || {};
opts.method = 'POST';
opts.data = data;
return this.request(path, opts, fn);
};
/**
* Issue a HEAD request
*
* @api private
*/
HTTPClient.prototype.head = function (path, opts, fn) {
if ('function' == typeof opts) {
fn = opts;
opts = {};
}
opts = opts || {};
opts.method = 'HEAD';
return this.request(path, opts, fn);
};
/**
* Performs a handshake (GET) request
*
* @api private
*/
HTTPClient.prototype.handshake = function (opts, fn) {
if ('function' == typeof opts) {
fn = opts;
opts = {};
}
return this.get('/socket.io/{protocol}', opts, function (res, data) {
fn && fn.apply(null, data.split(':'));
});
};
/**
* Generates a new client for the given port.
*
* @api private
*/
client = function (port) {
return new HTTPClient(port);
};
/**
* Create a socket.io server.
*/
create = function (cl) {
var manager = io.listen(cl.port);
manager.set('client store expiration', 0);
return manager;
};
/**
* WebSocket socket.io client.
*
* @api private
*/
function WSClient (port, sid, transport) {
this.sid = sid;
this.port = port;
this.transportName = transport || 'websocket';
WebSocket.call(
this
, 'ws://localhost:' + port + '/socket.io/'
+ io.protocol + '/' + this.transportName + '/' + sid
);
};
/**
* Inherits from WebSocket.
*/
WSClient.prototype.__proto__ = WebSocket.prototype;
/**
* Overrides message event emission.
*
* @api private
*/
WSClient.prototype.emit = function (name) {
var args = arguments;
if (name == 'message' || name == 'data') {
args[1] = parser.decodePacket(args[1].toString());
}
return WebSocket.prototype.emit.apply(this, arguments);
};
/**
* Writes a packet
*/
WSClient.prototype.packet = function (pack) {
this.write(parser.encodePacket(pack));
return this;
};
/**
* Creates a websocket client.
*
* @api public
*/
websocket = function (cl, sid, transport) {
return new WSClient(cl.port, sid, transport);
};

21
test/fixtures/cert.crt vendored Normal file
View File

@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAMUSOvlaeyQHMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTAxMTE2MDkzMjQ5WhcNMTMxMTE1MDkzMjQ5WjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAz+LXZOjcQCJq3+ZKUFabj71oo/ex/XsBcFqtBThjjTw9CVEVwfPQQp4X
wtPiB204vnYXwQ1/R2NdTQqCZu47l79LssL/u2a5Y9+0NEU3nQA5qdt+1FAE0c5o
exPimXOrR3GWfKz7PmZ2O0117IeCUUXPG5U8umhDe/4mDF4ZNJiKc404WthquTqg
S7rLQZHhZ6D0EnGnOkzlmxJMYPNHSOY1/6ivdNUUcC87awNEA3lgfhy25IyBK3QJ
c+aYKNTbt70Lery3bu2wWLFGtmNiGlQTS4JsxImRsECTI727ObS7/FWAQsqW+COL
0Sa5BuMFrFIpjPrEe0ih7vRRbdmXRwIDAQABo1AwTjAdBgNVHQ4EFgQUDnV4d6mD
tOnluLoCjkUHTX/n4agwHwYDVR0jBBgwFoAUDnV4d6mDtOnluLoCjkUHTX/n4agw
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAFwV4MQfTo+qMv9JMiyno
IEiqfOz4RgtmBqRnXUffcjS2dhc7/z+FPZnM79Kej8eLHoVfxCyWRHFlzm93vEdv
wxOCrD13EDOi08OOZfxWyIlCa6Bg8cMAKqQzd2OvQOWqlRWBTThBJIhWflU33izX
Qn5GdmYqhfpc+9ZHHGhvXNydtRQkdxVK2dZNzLBvBlLlRmtoClU7xm3A+/5dddeP
AQHEPtyFlUw49VYtZ3ru6KqPms7MKvcRhYLsy9rwSfuuniMlx4d0bDR7TOkw0QQS
A0N8MGQRQpzl4mw4jLzyM5d5QtuGBh2P6hPGa0YQxtI3RPT/p6ENzzBiAKXiSfzo
xw==
-----END CERTIFICATE-----

27
test/fixtures/key.key vendored Normal file
View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAz+LXZOjcQCJq3+ZKUFabj71oo/ex/XsBcFqtBThjjTw9CVEV
wfPQQp4XwtPiB204vnYXwQ1/R2NdTQqCZu47l79LssL/u2a5Y9+0NEU3nQA5qdt+
1FAE0c5oexPimXOrR3GWfKz7PmZ2O0117IeCUUXPG5U8umhDe/4mDF4ZNJiKc404
WthquTqgS7rLQZHhZ6D0EnGnOkzlmxJMYPNHSOY1/6ivdNUUcC87awNEA3lgfhy2
5IyBK3QJc+aYKNTbt70Lery3bu2wWLFGtmNiGlQTS4JsxImRsECTI727ObS7/FWA
QsqW+COL0Sa5BuMFrFIpjPrEe0ih7vRRbdmXRwIDAQABAoIBAGe4+9VqZfJN+dsq
8Osyuz01uQ8OmC0sAWTIqUlQgENIyf9rCJsUBlYmwR5BT6Z69XP6QhHdpSK+TiAR
XUz0EqG9HYzcxHIBaACP7j6iRoQ8R4kbbiWKo0z3WqQGIOqFjvD/mKEuQdE5mEYw
eOUCG6BnX1WY2Yr8WKd2AA/tp0/Y4d8z04u9eodMpSTbHTzYMJb5SbBN1vo6FY7q
8zSuO0BMzXlAxUsCwHsk1GQHFr8Oh3zIR7bQGtMBouI+6Lhh7sjFYsfxJboqMTBV
IKaA216M6ggHG7MU1/jeKcMGDmEfqQLQoyWp29rMK6TklUgipME2L3UD7vTyAVzz
xbVOpZkCgYEA8CXW4sZBBrSSrLR5SB+Ubu9qNTggLowOsC/kVKB2WJ4+xooc5HQo
mFhq1v/WxPQoWIxdYsfg2odlL+JclK5Qcy6vXmRSdAQ5lK9gBDKxZSYc3NwAw2HA
zyHCTK+I0n8PBYQ+yGcrxu0WqTGnlLW+Otk4CejO34WlgHwbH9bbY5UCgYEA3ZvT
C4+OoMHXlmICSt29zUrYiL33IWsR3/MaONxTEDuvgkOSXXQOl/8Ebd6Nu+3WbsSN
bjiPC/JyL1YCVmijdvFpl4gjtgvfJifs4G+QHvO6YfsYoVANk4u6g6rUuBIOwNK4
RwYxwDc0oysp+g7tPxoSgDHReEVKJNzGBe9NGGsCgYEA4O4QP4gCEA3B9BF2J5+s
n9uPVxmiyvZUK6Iv8zP4pThTBBMIzNIf09G9AHPQ7djikU2nioY8jXKTzC3xGTHM
GJZ5m6fLsu7iH+nDvSreDSeNkTBfZqGAvoGYQ8uGE+L+ZuRfCcXYsxIOT5s6o4c3
Dle2rVFpsuKzCY00urW796ECgYBn3go75+xEwrYGQSer6WR1nTgCV29GVYXKPooy
zmmMOT1Yw80NSkEw0pFD4cTyqVYREsTrPU0mn1sPfrOXxnGfZSVFpcR/Je9QVfQ7
eW7GYxwfom335aqHVj10SxRqteP+UoWWnHujCPz94VRKZMakBddYCIGSan+G6YdS
7sdmwwKBgBc2qj0wvGXDF2kCLwSGfWoMf8CS1+5fIiUIdT1e/+7MfDdbmLMIFVjF
QKS3zVViXCbrG5SY6wS9hxoc57f6E2A8vcaX6zy2xkZlGHQCpWRtEM5R01OWJQaH
HsHMmQZGUQVoDm1oRkDhrTFK4K3ukc3rAxzeTZ96utOQN8/KJsTv
-----END RSA PRIVATE KEY-----

99
test/hybi-common.js Normal file
View File

@@ -0,0 +1,99 @@
/**
* Returns a Buffer from a "ff 00 ff"-type hex string.
*/
getBufferFromHexString = function(byteStr) {
var bytes = byteStr.split(' ');
var buf = new Buffer(bytes.length);
for (var i = 0; i < bytes.length; ++i) {
buf[i] = parseInt(bytes[i], 16);
}
return buf;
}
/**
* Returns a hex string from a Buffer.
*/
getHexStringFromBuffer = function(data) {
var s = '';
for (var i = 0; i < data.length; ++i) {
s += padl(data[i].toString(16), 2, '0') + ' ';
}
return s.trim();
}
/**
* Splits a buffer in two parts.
*/
splitBuffer = function(buffer) {
var b1 = new Buffer(Math.ceil(buffer.length / 2));
buffer.copy(b1, 0, 0, b1.length);
var b2 = new Buffer(Math.floor(buffer.length / 2));
buffer.copy(b2, 0, b1.length, b1.length + b2.length);
return [b1, b2];
}
/**
* Performs hybi07+ type masking on a hex string or buffer.
*/
mask = function(buf, maskString) {
if (typeof buf == 'string') buf = new Buffer(buf);
var mask = getBufferFromHexString(maskString || '34 83 a8 68');
for (var i = 0; i < buf.length; ++i) {
buf[i] ^= mask[i % 4];
}
return buf;
}
/**
* Returns a hex string representing the length of a message
*/
getHybiLengthAsHexString = function(len, masked) {
if (len < 126) {
var buf = new Buffer(1);
buf[0] = (masked ? 0x80 : 0) | len;
}
else if (len < 65536) {
var buf = new Buffer(3);
buf[0] = (masked ? 0x80 : 0) | 126;
getBufferFromHexString(pack(4, len)).copy(buf, 1);
}
else {
var buf = new Buffer(9);
buf[0] = (masked ? 0x80 : 0) | 127;
getBufferFromHexString(pack(16, len)).copy(buf, 1);
}
return getHexStringFromBuffer(buf);
}
/**
* Unpacks a Buffer into a number.
*/
unpack = function(buffer) {
var n = 0;
for (var i = 0; i < buffer.length; ++i) {
n = (i == 0) ? buffer[i] : (n * 256) + buffer[i];
}
return n;
}
/**
* Returns a hex string, representing a specific byte count 'length', from a number.
*/
pack = function(length, number) {
return padl(number.toString(16), length, '0').replace(/(\d\d)/g, '$1 ').trim();
}
/**
* Left pads the string 's' to a total length of 'n' with char 'c'.
*/
padl = function(s, n, c) {
return new Array(1 + n - s.length).join(c) + s;
}

125
test/io.test.js Normal file
View File

@@ -0,0 +1,125 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Test dependencies.
*/
var sio = require('../')
, fs = require('fs')
, http = require('http')
, https = require('https')
, should = require('./common')
, ports = 15000;
/**
* Test.
*/
module.exports = {
'test that protocol version is present': function (done) {
sio.protocol.should.be.a('number');
done();
},
'test that default transports are present': function (done) {
sio.Manager.defaultTransports.should.be.an.instanceof(Array);
done();
},
'test that version is present': function (done) {
sio.version.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/);
done();
},
'test listening with a port': function (done) {
var cl = client(++ports)
, io = create(cl);
io.server.should.be.an.instanceof(http.Server);
cl.get('/', function (res, data) {
res.statusCode.should.eql(200);
data.should.eql('Welcome to socket.io.');
cl.end();
io.server.close();
done();
});
},
'test listening with a server': function (done) {
var server = http.createServer()
, io = sio.listen(server)
, port = ++ports
, cl = client(port);
server.listen(port);
cl.get('/socket.io', function (res, data) {
res.statusCode.should.eql(200);
data.should.eql('Welcome to socket.io.');
cl.end();
server.close();
done();
});
},
'test listening with a https server': function (done) {
var server = https.createServer({
key: fs.readFileSync(__dirname + '/fixtures/key.key')
, cert: fs.readFileSync(__dirname + '/fixtures/cert.crt')
}, function () { })
, io = sio.listen(server)
, port = ++ports;
server.listen(port);
var req = require('https').get({
host: 'localhost'
, port: port
, path: '/socket.io'
}, function (res) {
res.statusCode.should.eql(200);
var buf = '';
res.on('data', function (data) {
buf += data;
});
res.on('end', function () {
buf.should.eql('Welcome to socket.io.');
res.socket.end();
server.close();
done();
});
});
},
'test listening with no arguments listens on 80': function (done) {
try {
var io = sio.listen()
, cl = client(80);
cl.get('/socket.io', function (res) {
res.statusCode.should.eql(200);
cl.end();
io.server.close();
done();
});
done();
} catch (e) {
e.should.match(/EACCES/);
done();
}
}
};

View File

@@ -0,0 +1,54 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Test dependencies.
*/
require.paths.unshift(__dirname + '/../../lib');
var assertvanish = require('assertvanish')
, common = require('../common')
, ports = 15800;
function resultCallback (leaks, leakedSocket) {
if (leaks) {
console.error('Leak detected');
process.exit(1);
} else {
console.error('No leaks');
process.exit(0);
}
};
/**
* Test.
*/
var cl = client(++ports);
var io = create(cl);
io.sockets.on('connection', function (socket) {
console.log('connected');
socket.on('disconnect', function() {
console.log("client gone");
setTimeout(gc, 1000);
assertvanish(socket, 2000, {silent: true, callback: resultCallback});
});
});
setTimeout(function() {
cl.handshake(function (sid) {
var ws = websocket(cl, sid);
ws.on('open', function () {
console.log('open!');
setTimeout(function() {
ws.close();
}, 500);
});
});
}, 100);

589
test/manager.test.js Normal file
View File

@@ -0,0 +1,589 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Test dependencies.
*/
var sio = require('../')
, http = require('http')
, should = require('./common')
, ports = 15100;
/**
* Test.
*/
module.exports = {
'test setting and getting a configuration flag': function (done) {
var port = ++ports
, io = sio.listen(http.createServer());
io.set('a', 'b');
io.get('a').should.eql('b');
var port = ++ports
, io = sio.listen(http.createServer());
io.configure(function () {
io.set('a', 'b');
io.enable('tobi');
});
io.get('a').should.eql('b');
done();
},
'test enabling and disabling a configuration flag': function (done) {
var port = ++ports
, io = sio.listen(http.createServer());
io.enable('flag');
io.enabled('flag').should.be.true;
io.disabled('flag').should.be.false;
io.disable('flag');
var port = ++ports
, io = sio.listen(http.createServer());
io.configure(function () {
io.enable('tobi');
});
io.enabled('tobi').should.be.true;
done();
},
'test configuration callbacks with envs': function (done) {
var port = ++ports
, io = sio.listen(http.createServer());
process.env.NODE_ENV = 'development';
io.configure('production', function () {
io.set('ferret', 'tobi');
});
io.configure('development', function () {
io.set('ferret', 'jane');
});
io.get('ferret').should.eql('jane');
done();
},
'test configuration callbacks conserve scope': function (done) {
var port = ++ports
, io = sio.listen(http.createServer())
, calls = 0;
process.env.NODE_ENV = 'development';
io.configure(function () {
this.should.eql(io);
calls++;
});
io.configure('development', function () {
this.should.eql(io);
calls++;
});
calls.should.eql(2);
done();
},
'test configuration update notifications': function (done) {
var port = ++ports
, io = sio.listen(http.createServer())
, calls = 0;
io.on('set:foo', function () {
calls++;
});
io.set('foo', 'bar');
io.set('baz', 'bar');
calls.should.eql(1);
io.enable('foo');
io.disable('foo');
calls.should.eql(3);
done();
},
'test that normal requests are still served': function (done) {
var server = http.createServer(function (req, res) {
res.writeHead(200);
res.end('woot');
});
var io = sio.listen(server)
, port = ++ports
, cl = client(port);
server.listen(ports);
cl.get('/socket.io', function (res, data) {
res.statusCode.should.eql(200);
data.should.eql('Welcome to socket.io.');
cl.get('/woot', function (res, data) {
res.statusCode.should.eql(200);
data.should.eql('woot');
cl.end();
server.close();
done();
});
});
},
'test that you can disable clients': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.configure(function () {
io.disable('browser client');
});
cl.get('/socket.io/socket.io.js', function (res, data) {
res.statusCode.should.eql(200);
data.should.eql('Welcome to socket.io.');
cl.end();
io.server.close();
done();
});
},
'test handshake': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
cl.get('/socket.io/{protocol}/', function (res, data) {
res.statusCode.should.eql(200);
data.should.match(/([^:]+):([0-9]+)?:([0-9]+)?:(.+)/);
cl.end();
io.server.close();
done();
});
},
'test handshake with unsupported protocol version': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
cl.get('/socket.io/-1/', function (res, data) {
res.statusCode.should.eql(500);
data.should.match(/Protocol version not supported/);
cl.end();
io.server.close();
done();
});
},
'test authorization failure in handshake': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.configure(function () {
function auth (data, fn) {
fn(null, false);
};
io.set('authorization', auth);
});
cl.get('/socket.io/{protocol}/', function (res, data) {
res.statusCode.should.eql(403);
data.should.match(/handshake unauthorized/);
cl.end();
io.server.close();
done();
});
},
'test a handshake error': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.configure(function () {
function auth (data, fn) {
fn(new Error);
};
io.set('authorization', auth);
});
cl.get('/socket.io/{protocol}/', function (res, data) {
res.statusCode.should.eql(500);
data.should.match(/handshake error/);
cl.end();
io.server.close();
done();
});
},
'test that a referer is accepted for *:* origin': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.configure(function () {
io.set('origins', '*:*');
});
cl.get('/socket.io/{protocol}', { headers: { referer: 'http://foo.bar.com:82/something' } }, function (res, data) {
res.statusCode.should.eql(200);
cl.end();
io.server.close();
done();
});
},
'test that valid referer is accepted for addr:* origin': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.configure(function () {
io.set('origins', 'foo.bar.com:*');
});
cl.get('/socket.io/{protocol}', { headers: { referer: 'http://foo.bar.com/something' } }, function (res, data) {
res.statusCode.should.eql(200);
cl.end();
io.server.close();
done();
});
},
'test that a referer with implicit port 80 is accepted for foo.bar.com:80 origin': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.configure(function () {
io.set('origins', 'foo.bar.com:80');
});
cl.get('/socket.io/{protocol}', { headers: { referer: 'http://foo.bar.com/something' } }, function (res, data) {
res.statusCode.should.eql(200);
cl.end();
io.server.close();
done();
});
},
'test that erroneous referer is denied for addr:* origin': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.configure(function () {
io.set('origins', 'foo.bar.com:*');
});
cl.get('/socket.io/{protocol}', { headers: { referer: 'http://baz.bar.com/something' } }, function (res, data) {
res.statusCode.should.eql(403);
cl.end();
io.server.close();
done();
});
},
'test that valid referer port is accepted for addr:port origin': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.configure(function () {
io.set('origins', 'foo.bar.com:81');
});
cl.get('/socket.io/{protocol}', { headers: { referer: 'http://foo.bar.com:81/something' } }, function (res, data) {
res.statusCode.should.eql(200);
cl.end();
io.server.close();
done();
});
},
'test that erroneous referer port is denied for addr:port origin': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.configure(function () {
io.set('origins', 'foo.bar.com:81');
});
cl.get('/socket.io/{protocol}', { headers: { referer: 'http://foo.bar.com/something' } }, function (res, data) {
res.statusCode.should.eql(403);
cl.end();
io.server.close();
done();
});
},
'test handshake cross domain access control': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port)
, headers = {
Origin: 'http://example.org:1337'
, Cookie: 'name=value'
};
cl.get('/socket.io/{protocol}/', { headers:headers }, function (res, data) {
res.statusCode.should.eql(200);
res.headers['access-control-allow-origin'].should.eql('http://example.org:1337');
res.headers['access-control-allow-credentials'].should.eql('true');
cl.end();
io.server.close();
done();
});
},
'test limiting the supported transports for a manager': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.configure(function () {
io.set('transports', ['tobi', 'jane']);
});
cl.get('/socket.io/{protocol}/', function (res, data) {
res.statusCode.should.eql(200);
data.should.match(/([^:]+):([0-9]+)?:([0-9]+)?:tobi,jane/);
cl.end();
io.server.close();
done();
});
},
'test setting a custom close timeout': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.configure(function () {
io.set('close timeout', 66);
});
cl.get('/socket.io/{protocol}/', function (res, data) {
res.statusCode.should.eql(200);
data.should.match(/([^:]+):([0-9]+)?:66?:(.*)/);
cl.end();
io.server.close();
done();
});
},
'test setting a custom heartbeat timeout': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.configure(function () {
io.set('heartbeat timeout', 33);
});
cl.get('/socket.io/{protocol}/', function (res, data) {
res.statusCode.should.eql(200);
data.should.match(/([^:]+):33:([0-9]+)?:(.*)/);
cl.end();
io.server.close();
done();
});
},
'test disabling timeouts': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.configure(function () {
io.set('heartbeat timeout', null);
io.set('close timeout', '');
});
cl.get('/socket.io/{protocol}/', function (res, data) {
res.statusCode.should.eql(200);
data.should.match(/([^:]+)::?:(.*)/);
cl.end();
io.server.close();
done();
});
},
'test disabling heartbeats': function (done) {
var port = ++ports
, cl = client(port)
, io = create(cl)
, messages = 0
, beat = false
, ws;
io.configure(function () {
io.disable('heartbeats');
io.set('heartbeat interval', .05);
io.set('heartbeat timeout', .05);
io.set('close timeout', .05);
});
io.sockets.on('connection', function (socket) {
setTimeout(function () {
socket.disconnect();
}, io.get('heartbeat timeout') * 1000 + 100);
socket.on('disconnect', function (reason) {
beat.should.be.false;
ws.finishClose();
cl.end();
io.server.close();
done();
});
});
cl.get('/socket.io/{protocol}/', function (res, data) {
res.statusCode.should.eql(200);
data.should.match(/([^:]+)::[\.0-9]+:(.*)/);
cl.handshake(function (sid) {
ws = websocket(cl, sid);
ws.on('message', function (packet) {
if (++messages == 1) {
packet.type.should.eql('connect');
} else if (packet.type == 'heartbeat'){
beat = true;
}
});
});
});
},
'no duplicate room members': function (done) {
var port = ++ports
, io = sio.listen(port);
Object.keys(io.rooms).length.should.equal(0);
io.onJoin(123, 'foo');
io.rooms.foo.length.should.equal(1);
io.onJoin(123, 'foo');
io.rooms.foo.length.should.equal(1);
io.onJoin(124, 'foo');
io.rooms.foo.length.should.equal(2);
io.onJoin(124, 'foo');
io.rooms.foo.length.should.equal(2);
io.onJoin(123, 'bar');
io.rooms.foo.length.should.equal(2);
io.rooms.bar.length.should.equal(1);
io.onJoin(123, 'bar');
io.rooms.foo.length.should.equal(2);
io.rooms.bar.length.should.equal(1);
io.onJoin(124, 'bar');
io.rooms.foo.length.should.equal(2);
io.rooms.bar.length.should.equal(2);
io.onJoin(124, 'bar');
io.rooms.foo.length.should.equal(2);
io.rooms.bar.length.should.equal(2);
process.nextTick(function() {
io.server.close();
done();
});
},
'test passing options directly to the Manager through listen': function (done) {
var port = ++ports
, io = sio.listen(port, { resource: '/my resource', custom: 'opt' });
io.get('resource').should.equal('/my resource');
io.get('custom').should.equal('opt');
process.nextTick(function() {
io.server.close();
done();
});
},
'test disabling the log': function (done) {
var port = ++ports
, io = sio.listen(port, { log: false })
, _console = console.log
, calls = 0;
// the logger uses console.log to output data, override it to see if get's
// used
console.log = function () { ++calls };
io.log.debug('test');
io.log.log('testing');
console.log = _console;
calls.should.equal(0);
process.nextTick(function() {
io.server.close();
done();
});
},
'test disabling logging with colors': function (done) {
var port = ++ports
, io = sio.listen(port, { 'log colors': false })
, _console = console.log
, calls = 0;
// the logger uses console.log to output data, override it to see if get's
// used
console.log = function (data) {
++calls;
data.indexOf('\033').should.equal(-1);
};
io.log.debug('test');
io.log.log('testing');
console.log = _console;
calls.should.equal(2);
process.nextTick(function() {
io.server.close();
done();
});
}
};

327
test/namespace.test.js Normal file
View File

@@ -0,0 +1,327 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Test dependencies.
*/
var sio = require('socket.io')
, should = require('./common')
, ports = 15700;
/**
* Test.
*/
module.exports = {
'namespace pass no authentication': function (done) {
var cl = client(++ports)
, io = create(cl)
, ws;
io.of('/a')
.on('connection', function (socket) {
cl.end();
ws.finishClose();
io.server.close()
done();
});
cl.handshake(function (sid) {
ws = websocket(cl, sid);
ws.on('open', function () {
ws.packet({
type: 'connect'
, endpoint: '/a'
});
})
});
},
'namespace pass authentication': function (done) {
var cl = client(++ports)
, io = create(cl)
, ws;
io.of('/a')
.authorization(function (data, fn) {
fn(null, true);
})
.on('connection', function (socket) {
cl.end();
ws.finishClose();
io.server.close()
done();
});
cl.handshake(function (sid) {
ws = websocket(cl, sid);
ws.on('open', function () {
ws.packet({
type: 'connect'
, endpoint: '/a'
});
})
});
},
'namespace authentication handshake data': function (done) {
var cl = client(++ports)
, io = create(cl)
, ws;
io.of('/a')
.authorization(function (data, fn) {
data.foo = 'bar';
fn(null, true);
})
.on('connection', function (socket) {
(!!socket.handshake.address.address).should.be.true;
(!!socket.handshake.address.port).should.be.true;
socket.handshake.headers.host.should.equal('localhost');
socket.handshake.headers.connection.should.equal('keep-alive');
socket.handshake.time.should.match(/GMT/);
socket.handshake.foo.should.equal('bar');
cl.end();
ws.finishClose();
io.server.close()
done();
});
cl.handshake(function (sid) {
ws = websocket(cl, sid);
ws.on('open', function () {
ws.packet({
type: 'connect'
, endpoint: '/a'
});
})
});
},
'namespace fail authentication': function (done) {
var cl = client(++ports)
, io = create(cl)
, calls = 0
, ws;
io.of('/a')
.authorization(function (data, fn) {
fn(null, false);
})
.on('connection', function (socket) {
throw new Error('Should not be called');
});
cl.handshake(function (sid) {
ws = websocket(cl, sid);
ws.on('open', function () {
ws.packet({
type: 'connect'
, endpoint: '/a'
});
});
ws.on('message', function (data) {
if (data.endpoint == '/a') {
data.type.should.eql('error');
data.reason.should.eql('unauthorized')
cl.end();
ws.finishClose();
io.server.close()
done();
}
})
});
},
'broadcasting sends and emits on a namespace': function (done) {
var cl = client(++ports)
, io = create(cl)
, calls = 0
, connect = 0
, message = 0
, events = 0
, expected = 5
, ws1
, ws2;
io.of('a')
.on('connection', function (socket){
if (connect < 2) {
return;
}
socket.broadcast.emit('b', 'test');
socket.broadcast.json.emit('json', {foo:'bar'});
socket.broadcast.send('foo');
});
function finish () {
connect.should.equal(2);
message.should.equal(1);
events.should.equal(2);
cl.end();
ws1.finishClose();
ws2.finishClose();
io.server.close();
done();
}
cl.handshake(function (sid) {
ws1 = websocket(cl, sid);
ws1.on('message', function (data) {
if (data.type === 'connect') {
if (connect == 0) {
cl.handshake(function (sid) {
ws2 = websocket(cl, sid);
ws2.on('open', function () {
ws2.packet({
type: 'connect'
, endpoint: 'a'
});
});
});
}
++connect;
if (++calls === expected) finish();
}
if (data.type === 'message') {
++message;
if (++calls === expected) finish();
}
if (data.type === 'event') {
if (data.name === 'b' || data.name === 'json') ++events;
if (++calls === expected) finish();
}
});
ws1.on('open', function() {
ws1.packet({
type: 'connect'
, endpoint: 'a'
});
});
})
},
'joining rooms inside a namespace': function (done) {
var cl = client(++ports)
, io = create(cl)
, calls = 0
, ws;
io.of('/foo').on('connection', function (socket) {
socket.join('foo.bar');
this.in('foo.bar').emit('baz', 'pewpew');
});
cl.handshake(function (sid) {
ws = websocket(cl, sid);
ws.on('open', function (){
ws.packet({
type: 'connect'
, endpoint: '/foo'
});
});
ws.on('message', function (data) {
if (data.type === 'event') {
data.name.should.equal('baz');
cl.end();
ws.finishClose();
io.server.close();
done();
}
});
})
},
'ignoring blacklisted events': function (done) {
var cl = client(++ports)
, io = create(cl)
, calls = 0
, ws;
io.set('heartbeat interval', 1);
io.set('blacklist', ['foobar']);
io.sockets.on('connection', function (socket) {
socket.on('foobar', function () {
calls++;
});
});
cl.handshake(function (sid) {
ws = websocket(cl, sid);
ws.on('open', function (){
ws.packet({
type: 'event'
, name: 'foobar'
, endpoint: ''
});
});
ws.on('message', function (data) {
if (data.type === 'heartbeat') {
cl.end();
ws.finishClose();
io.server.close();
calls.should.equal(0);
done();
}
});
});
},
'disconnecting from namespace only': function (done) {
var cl = client(++ports)
, io = create(cl)
, ws1
, ws2;
io.of('/foo').on('connection', function (socket) {
socket.disconnect();
});
cl.handshake(function (sid) {
ws1 = websocket(cl, sid);
ws1.on('open', function () {
ws1.packet({
type: 'connect'
, endpoint: '/bar'
});
cl.handshake(function (sid) {
ws2 = websocket(cl, sid);
ws2.on('open', function () {
ws2.packet({
type: 'connect'
, endpoint: '/foo'
});
});
ws2.on('message', function (data) {
if ('disconnect' === data.type) {
cl.end();
ws1.finishClose();
ws2.finishClose();
io.server.close();
data.endpoint.should.eql('/foo');
done();
}
});
});
});
});
}
};

356
test/parser.test.js Normal file
View File

@@ -0,0 +1,356 @@
/**
* Test dependencies.
*/
var parser = require('../').parser
, decode = parser.decode
, should = require('./common');
/**
* Test.
*/
module.exports = {
'decoding error packet': function () {
parser.decodePacket('7:::').should.eql({
type: 'error'
, reason: ''
, advice: ''
, endpoint: ''
});
},
'decoding error packet with reason': function () {
parser.decodePacket('7:::0').should.eql({
type: 'error'
, reason: 'transport not supported'
, advice: ''
, endpoint: ''
});
},
'decoding error packet with reason and advice': function () {
parser.decodePacket('7:::2+0').should.eql({
type: 'error'
, reason: 'unauthorized'
, advice: 'reconnect'
, endpoint: ''
});
},
'decoding error packet with endpoint': function () {
parser.decodePacket('7::/woot').should.eql({
type: 'error'
, reason: ''
, advice: ''
, endpoint: '/woot'
});
},
'decoding ack packet': function () {
parser.decodePacket('6:::140').should.eql({
type: 'ack'
, ackId: '140'
, endpoint: ''
, args: []
});
},
'decoding ack packet with args': function () {
parser.decodePacket('6:::12+["woot","wa"]').should.eql({
type: 'ack'
, ackId: '12'
, endpoint: ''
, args: ['woot', 'wa']
});
},
'decoding ack packet with bad json': function () {
var thrown = false;
try {
parser.decodePacket('6:::1+{"++]').should.eql({
type: 'ack'
, ackId: '1'
, endpoint: ''
, args: []
});
} catch (e) {
thrown = true;
}
thrown.should.be.false;
},
'decoding json packet': function () {
parser.decodePacket('4:::"2"').should.eql({
type: 'json'
, endpoint: ''
, data: '2'
});
},
'decoding json packet with message id and ack data': function () {
parser.decodePacket('4:1+::{"a":"b"}').should.eql({
type: 'json'
, id: '1'
, ack: 'data'
, endpoint: ''
, data: { a: 'b' }
});
},
'decoding an event packet': function () {
parser.decodePacket('5:::{"name":"woot"}').should.eql({
type: 'event'
, name: 'woot'
, endpoint: ''
, args: []
});
},
'decoding an event packet with message id and ack': function () {
parser.decodePacket('5:1+::{"name":"tobi"}').should.eql({
type: 'event'
, id: '1'
, ack: 'data'
, endpoint: ''
, name: 'tobi'
, args: []
});
},
'decoding an event packet with data': function () {
parser.decodePacket('5:::{"name":"edwald","args":[{"a": "b"},2,"3"]}')
.should.eql({
type: 'event'
, name: 'edwald'
, endpoint: ''
, args: [{a: 'b'}, 2, '3']
});
},
'decoding a message packet': function () {
parser.decodePacket('3:::woot').should.eql({
type: 'message'
, endpoint: ''
, data: 'woot'
});
},
'decoding a message packet with id and endpoint': function () {
parser.decodePacket('3:5:/tobi').should.eql({
type: 'message'
, id: '5'
, ack: true
, endpoint: '/tobi'
, data: ''
});
},
'decoding a heartbeat packet': function () {
parser.decodePacket('2:::').should.eql({
type: 'heartbeat'
, endpoint: ''
});
},
'decoding a connection packet': function () {
parser.decodePacket('1::/tobi').should.eql({
type: 'connect'
, endpoint: '/tobi'
, qs: ''
});
},
'decoding a connection packet with query string': function () {
parser.decodePacket('1::/test:?test=1').should.eql({
type: 'connect'
, endpoint: '/test'
, qs: '?test=1'
});
},
'decoding a disconnection packet': function () {
parser.decodePacket('0::/woot').should.eql({
type: 'disconnect'
, endpoint: '/woot'
});
},
'encoding error packet': function () {
parser.encodePacket({
type: 'error'
, reason: ''
, advice: ''
, endpoint: ''
}).should.eql('7::');
},
'encoding error packet with reason': function () {
parser.encodePacket({
type: 'error'
, reason: 'transport not supported'
, advice: ''
, endpoint: ''
}).should.eql('7:::0');
},
'encoding error packet with reason and advice': function () {
parser.encodePacket({
type: 'error'
, reason: 'unauthorized'
, advice: 'reconnect'
, endpoint: ''
}).should.eql('7:::2+0');
},
'encoding error packet with endpoint': function () {
parser.encodePacket({
type: 'error'
, reason: ''
, advice: ''
, endpoint: '/woot'
}).should.eql('7::/woot');
},
'encoding ack packet': function () {
parser.encodePacket({
type: 'ack'
, ackId: '140'
, endpoint: ''
, args: []
}).should.eql('6:::140');
},
'encoding ack packet with args': function () {
parser.encodePacket({
type: 'ack'
, ackId: '12'
, endpoint: ''
, args: ['woot', 'wa']
}).should.eql('6:::12+["woot","wa"]');
},
'encoding json packet': function () {
parser.encodePacket({
type: 'json'
, endpoint: ''
, data: '2'
}).should.eql('4:::"2"');
},
'encoding json packet with message id and ack data': function () {
parser.encodePacket({
type: 'json'
, id: '1'
, ack: 'data'
, endpoint: ''
, data: { a: 'b' }
}).should.eql('4:1+::{"a":"b"}');
},
'encoding an event packet': function () {
parser.encodePacket({
type: 'event'
, name: 'woot'
, endpoint: ''
, args: []
}).should.eql('5:::{"name":"woot"}');
},
'encoding an event packet with message id and ack': function () {
parser.encodePacket({
type: 'event'
, id: '1'
, ack: 'data'
, endpoint: ''
, name: 'tobi'
, args: []
}).should.eql('5:1+::{"name":"tobi"}');
},
'encoding an event packet with data': function () {
parser.encodePacket({
type: 'event'
, name: 'edwald'
, endpoint: ''
, args: [{a: 'b'}, 2, '3']
}).should.eql('5:::{"name":"edwald","args":[{"a":"b"},2,"3"]}');
},
'encoding a message packet': function () {
parser.encodePacket({
type: 'message'
, endpoint: ''
, data: 'woot'
}).should.eql('3:::woot');
},
'encoding a message packet with id and endpoint': function () {
parser.encodePacket({
type: 'message'
, id: '5'
, ack: true
, endpoint: '/tobi'
, data: ''
}).should.eql('3:5:/tobi');
},
'encoding a heartbeat packet': function () {
parser.encodePacket({
type: 'heartbeat'
, endpoint: ''
}).should.eql('2::');
},
'encoding a connection packet': function () {
parser.encodePacket({
type: 'connect'
, endpoint: '/tobi'
, qs: ''
}).should.eql('1::/tobi');
},
'encoding a connection packet with query string': function () {
parser.encodePacket({
type: 'connect'
, endpoint: '/test'
, qs: '?test=1'
}).should.eql('1::/test:?test=1');
},
'encoding a disconnection packet': function () {
parser.encodePacket({
type: 'disconnect'
, endpoint: '/woot'
}).should.eql('0::/woot');
},
'test decoding a payload': function () {
parser.decodePayload('\ufffd5\ufffd3:::5\ufffd7\ufffd3:::53d'
+ '\ufffd3\ufffd0::').should.eql([
{ type: 'message', data: '5', endpoint: '' }
, { type: 'message', data: '53d', endpoint: '' }
, { type: 'disconnect', endpoint: '' }
]);
},
'test encoding a payload': function () {
parser.encodePayload([
parser.encodePacket({ type: 'message', data: '5', endpoint: '' })
, parser.encodePacket({ type: 'message', data: '53d', endpoint: '' })
]).should.eql('\ufffd5\ufffd3:::5\ufffd7\ufffd3:::53d')
},
'test decoding newline': function () {
parser.decodePacket('3:::\n').should.eql({
type: 'message'
, endpoint: ''
, data: '\n'
});
}
};

File diff suppressed because it is too large Load Diff

549
test/static.test.js Normal file
View File

@@ -0,0 +1,549 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Test dependencies.
*/
var sio = require('../')
, should = require('./common')
, ports = 15400;
/**
* Test.
*/
module.exports = {
'test that the default static files are available': function (done) {
var port = ++ports
, io = sio.listen(port);
(!!io.static.has('/socket.io.js')).should.be.true;
(!!io.static.has('/socket.io.v1.0.0.js')).should.be.true;
(!!io.static.has('/socket.io+xhr-polling.js')).should.be.true;
(!!io.static.has('/socket.io+xhr-polling.v1.0.0.js')).should.be.true;
(!!io.static.has('/static/flashsocket/WebSocketMain.swf')).should.be.true;
(!!io.static.has('/static/flashsocket/WebSocketMainInsecure.swf')).should.be.true;
process.nextTick(function() {
io.server.close();
done();
});
},
'test that static files are correctly looked up': function (done) {
var port = ++ports
, io = sio.listen(port);
(!!io.static.has('/socket.io.js')).should.be.true;
(!!io.static.has('/invalidfilehereplease.js')).should.be.false;
process.nextTick(function() {
io.server.close();
done();
});
},
'test that the client is served': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
cl.get('/socket.io/socket.io.js', function (res, data) {
res.headers['content-type'].should.eql('application/javascript');
res.headers['content-length'].should.match(/([0-9]+)/);
should.strictEqual(res.headers.etag, undefined);
data.should.match(/XMLHttpRequest/);
cl.end();
io.server.close();
done();
});
},
'test that the custom build client is served': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.enable('browser client etag');
cl.get('/socket.io/socket.io+websocket.js', function (res, data) {
res.headers['content-type'].should.eql('application/javascript');
res.headers['content-length'].should.match(/([0-9]+)/);
res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/);
data.should.match(/XMLHttpRequest/);
data.should.match(/WS\.prototype\.name/);
data.should.not.match(/Flashsocket\.prototype\.name/);
data.should.not.match(/HTMLFile\.prototype\.name/);
data.should.not.match(/JSONPPolling\.prototype\.name/);
data.should.not.match(/XHRPolling\.prototype\.name/);
cl.end();
io.server.close();
done();
});
},
'test that the client is build with the enabled transports': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.set('transports', ['websocket']);
cl.get('/socket.io/socket.io.js', function (res, data) {
res.headers['content-type'].should.eql('application/javascript');
res.headers['content-length'].should.match(/([0-9]+)/);
data.should.match(/XMLHttpRequest/);
data.should.match(/WS\.prototype\.name/);
data.should.not.match(/Flashsocket\.prototype\.name/);
data.should.not.match(/HTMLFile\.prototype\.name/);
data.should.not.match(/JSONPPolling\.prototype\.name/);
data.should.not.match(/XHRPolling\.prototype\.name/);
cl.end();
io.server.close();
done();
});
},
'test that the client cache is cleared when transports change': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.set('transports', ['websocket']);
cl.get('/socket.io/socket.io.js', function (res, data) {
res.headers['content-type'].should.eql('application/javascript');
res.headers['content-length'].should.match(/([0-9]+)/);
data.should.match(/XMLHttpRequest/);
data.should.match(/WS\.prototype\.name/);
data.should.not.match(/Flashsocket\.prototype\.name/);
data.should.not.match(/HTMLFile\.prototype\.name/);
data.should.not.match(/JSONPPolling\.prototype\.name/);
data.should.not.match(/XHRPolling\.prototype\.name/);
io.set('transports', ['xhr-polling']);
should.strictEqual(io.static.cache['/socket.io.js'], undefined);
cl.get('/socket.io/socket.io.js', function (res, data) {
res.headers['content-type'].should.eql('application/javascript');
res.headers['content-length'].should.match(/([0-9]+)/);
data.should.match(/XMLHttpRequest/);
data.should.match(/XHRPolling\.prototype\.name/);
data.should.not.match(/Flashsocket\.prototype\.name/);
data.should.not.match(/HTMLFile\.prototype\.name/);
data.should.not.match(/JSONPPolling\.prototype\.name/);
data.should.not.match(/WS\.prototype\.name/);
cl.end();
io.server.close();
done();
});
});
},
'test that the client etag is served': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.enable('browser client etag');
cl.get('/socket.io/socket.io.js', function (res, data) {
res.headers['content-type'].should.eql('application/javascript');
res.headers['content-length'].should.match(/([0-9]+)/);
res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/);
data.should.match(/XMLHttpRequest/);
cl.end();
io.server.close();
done();
});
},
'test that the client etag is changed for new transports': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.set('transports', ['websocket']);
io.enable('browser client etag');
cl.get('/socket.io/socket.io.js', function (res, data) {
var wsEtag = res.headers.etag;
io.set('transports', ['xhr-polling']);
cl.get('/socket.io/socket.io.js', function (res, data) {
res.headers.etag.should.not.equal(wsEtag);
cl.end();
io.server.close();
done();
});
});
},
'test that the client is served with gzip': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.enable('browser client gzip');
cl.get('/socket.io/socket.io.js', {
headers: {
'accept-encoding': 'deflate, gzip'
}
}
, function (res, data) {
res.headers['content-type'].should.eql('application/javascript');
res.headers['content-encoding'].should.eql('gzip');
res.headers['content-length'].should.match(/([0-9]+)/);
cl.end();
io.server.close();
done();
}
);
},
'test that the cached client is served': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
cl.get('/socket.io/socket.io.js', function (res, data) {
res.headers['content-type'].should.eql('application/javascript');
res.headers['content-length'].should.match(/([0-9]+)/);
should.strictEqual(res.headers.etag, undefined);
data.should.match(/XMLHttpRequest/);
var static = io.static;
static.cache['/socket.io.js'].content.should.match(/XMLHttpRequest/);
cl.get('/socket.io/socket.io.js', function (res, data) {
res.headers['content-type'].should.eql('application/javascript');
res.headers['content-length'].should.match(/([0-9]+)/);
should.strictEqual(res.headers.etag, undefined);
data.should.match(/XMLHttpRequest/);
cl.end();
io.server.close();
done();
});
});
},
'test that the client is not cached': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.static.add('/random.js', function (path, callback) {
var random = Math.floor(Date.now() * Math.random()).toString();
callback(null, new Buffer(random));
});
io.disable('browser client cache');
cl.get('/socket.io/random.js', function (res, data) {
res.headers['content-type'].should.eql('application/javascript');
res.headers['content-length'].should.match(/([0-9]+)/);
should.strictEqual(res.headers.etag, undefined);
cl.get('/socket.io/random.js', function (res, random) {
res.headers['content-type'].should.eql('application/javascript');
res.headers['content-length'].should.match(/([0-9]+)/);
should.strictEqual(res.headers.etag, undefined);
data.should.not.equal(random);
cl.end();
io.server.close();
done();
});
});
},
'test that the cached client etag is served': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.enable('browser client etag');
cl.get('/socket.io/socket.io.js', function (res, data) {
res.headers['content-type'].should.eql('application/javascript');
res.headers['content-length'].should.match(/([0-9]+)/);
res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/);
data.should.match(/XMLHttpRequest/);
var static = io.static
, cache = static.cache['/socket.io.js'];
cache.content.toString().should.match(/XMLHttpRequest/);
Buffer.isBuffer(cache.content).should.be.true;
cl.get('/socket.io/socket.io.js', function (res, data) {
res.headers['content-type'].should.eql('application/javascript');
res.headers['content-length'].should.match(/([0-9]+)/);
res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/);
data.should.match(/XMLHttpRequest/);
cl.end();
io.server.close();
done();
});
});
},
'test that the cached client sends a 304 header': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.enable('browser client etag');
cl.get('/socket.io/socket.io.js', function (res, data) {
cl.get('/socket.io/socket.io.js', {
headers: {
'if-none-match': res.headers.etag
}
}, function (res, data) {
res.statusCode.should.eql(304);
cl.end();
io.server.close();
done();
}
);
});
},
'test that client minification works': function (done) {
// server 1
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
// server 2
var port = ++ports
, io2 = sio.listen(port)
, cl2 = client(port);
io.enable('browser client minification');
cl.get('/socket.io/socket.io.js', function (res, data) {
var length = data.length;
cl.end();
io.server.close();
cl2.get('/socket.io/socket.io.js', function (res, data) {
res.headers['content-type'].should.eql('application/javascript');
res.headers['content-length'].should.match(/([0-9]+)/);
should.strictEqual(res.headers.etag, undefined);
data.should.match(/XMLHttpRequest/);
data.length.should.be.greaterThan(length);
cl2.end();
io2.server.close();
done();
});
});
},
'test that the WebSocketMain.swf is served': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
cl.get('/socket.io/static/flashsocket/WebSocketMain.swf', function (res, data) {
res.headers['content-type'].should.eql('application/x-shockwave-flash');
res.headers['content-length'].should.match(/([0-9]+)/);
should.strictEqual(res.headers.etag, undefined);
var static = io.static
, cache = static.cache['/static/flashsocket/WebSocketMain.swf'];
Buffer.isBuffer(cache.content).should.be.true;
cl.end();
io.server.close();
done();
});
},
'test that the WebSocketMainInsecure.swf is served': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
cl.get('/socket.io/static/flashsocket/WebSocketMainInsecure.swf', function (res, data) {
res.headers['content-type'].should.eql('application/x-shockwave-flash');
res.headers['content-length'].should.match(/([0-9]+)/);
should.strictEqual(res.headers.etag, undefined);
var static = io.static
, cache = static.cache['/static/flashsocket/WebSocketMainInsecure.swf'];
Buffer.isBuffer(cache.content).should.be.true;
cl.end();
io.server.close();
done();
});
},
'test that swf files are not served with gzip': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.enable('browser client gzip');
cl.get('/socket.io/static/flashsocket/WebSocketMain.swf', {
headers: {
'accept-encoding': 'deflate, gzip'
}
}
, function (res, data) {
res.headers['content-type'].should.eql('application/x-shockwave-flash');
res.headers['content-length'].should.match(/([0-9]+)/);
should.strictEqual(res.headers['content-encoding'], undefined);
cl.end();
io.server.close();
done();
}
);
},
'test that you can serve custom clients': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.set('browser client handler', function (req, res) {
res.writeHead(200, {
'Content-Type': 'application/javascript'
, 'Content-Length': 13
, 'ETag': '1.0'
});
res.end('custom_client');
});
cl.get('/socket.io/socket.io.js', function (res, data) {
res.headers['content-type'].should.eql('application/javascript');
res.headers['content-length'].should.eql('13');
res.headers.etag.should.eql('1.0');
data.should.eql('custom_client');
cl.end();
io.server.close();
done();
});
},
'test that HEAD requests work': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
cl.head('/socket.io/socket.io.js', function (res, data) {
res.headers['content-type'].should.eql('application/javascript');
res.headers['content-length'].should.match(/([0-9]+)/);
data.should.eql('');
cl.end();
io.server.close()
done();
});
},
'test that a versioned client is served': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
cl.get('/socket.io/socket.io.v0.8.9.js', function (res, data) {
res.headers['content-type'].should.eql('application/javascript');
res.headers['content-length'].should.match(/([0-9]+)/);
res.headers['cache-control']
.indexOf(io.get('browser client expires')).should.be.above(-1);
data.should.match(/XMLHttpRequest/);
cl.end();
io.server.close();
done();
});
},
'test that a custom versioned build client is served': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.set('browser client expires', 1337);
cl.get('/socket.io/socket.io+websocket.v0.8.10.js', function (res, data) {
res.headers['content-type'].should.eql('application/javascript');
res.headers['content-length'].should.match(/([0-9]+)/);
res.headers['cache-control']
.indexOf(io.get('browser client expires')).should.be.above(-1);
data.should.match(/XMLHttpRequest/);
data.should.match(/WS\.prototype\.name/);
data.should.not.match(/Flashsocket\.prototype\.name/);
data.should.not.match(/HTMLFile\.prototype\.name/);
data.should.not.match(/JSONPPolling\.prototype\.name/);
data.should.not.match(/XHRPolling\.prototype\.name/);
cl.end();
io.server.close();
done();
});
},
'test that etags are ignored for versioned requests': function (done) {
var port = ++ports
, io = sio.listen(port)
, cl = client(port);
io.enable('browser client etag');
cl.get('/socket.io/socket.io.v0.8.9.js', function (res, data) {
should.strictEqual(res.headers.etag, undefined);
res.headers['content-type'].should.eql('application/javascript');
res.headers['content-length'].should.match(/([0-9]+)/);
res.headers['cache-control']
.indexOf(io.get('browser client expires')).should.be.above(-1);
data.should.match(/XMLHttpRequest/);
cl.end();
io.server.close();
done();
});
},
};

190
test/stores.memory.test.js Normal file
View File

@@ -0,0 +1,190 @@
/**
* Test dependencies
*
* @api private
*/
var sio = require('../')
, should = require('should')
, MemoryStore = sio.MemoryStore;
/**
* Test.
*/
module.exports = {
'test storing data for a client': function (done) {
var store = new MemoryStore
, client = store.client('test');
client.id.should.equal('test');
client.set('a', 'b', function (err) {
should.strictEqual(err, null);
client.get('a', function (err, val) {
should.strictEqual(err, null);
val.should.eql('b');
client.has('a', function (err, has) {
should.strictEqual(err, null);
has.should.be.true;
client.has('b', function (err, has) {
should.strictEqual(err, null);
has.should.be.false;
client.del('a', function (err) {
should.strictEqual(err, null);
client.has('a', function (err, has) {
should.strictEqual(err, null);
has.should.be.false;
client.set('b', 'c', function (err) {
should.strictEqual(err, null);
client.set('c', 'd', function (err) {
should.strictEqual(err, null);
client.get('b', function (err, val) {
should.strictEqual(err, null);
val.should.equal('c');
client.get('c', function (err, val) {
should.strictEqual(err, null);
val.should.equal('d');
store.destroy();
done();
});
});
});
});
});
});
});
});
});
});
},
'test cleaning up clients data': function (done) {
var rand1 = Math.abs(Math.random() * Date.now() | 0)
, rand2 = Math.abs(Math.random() * Date.now() | 0);
var store = new MemoryStore()
, client1 = store.client(rand1)
, client2 = store.client(rand2);
client1.set('a', 'b', function (err) {
should.strictEqual(err, null);
client2.set('c', 'd', function (err) {
should.strictEqual(err, null);
client1.has('a', function (err, val) {
should.strictEqual(err, null);
val.should.be.true;
client2.has('c', function (err, val) {
should.strictEqual(err, null);
val.should.be.true;
store.destroy();
var newstore = new MemoryStore()
, newclient1 = newstore.client(rand1)
, newclient2 = newstore.client(rand2);
newclient1.has('a', function (err, val) {
should.strictEqual(err, null);
val.should.be.false;
newclient2.has('c', function (err, val) {
should.strictEqual(err, null);
val.should.be.false;
newstore.destroy();
done();
});
});
});
});
});
});
},
'test cleaning up a particular client': function (done) {
var rand1 = Math.abs(Math.random() * Date.now() | 0)
, rand2 = Math.abs(Math.random() * Date.now() | 0);
var store = new MemoryStore()
, client1 = store.client(rand1)
, client2 = store.client(rand2);
client1.set('a', 'b', function (err) {
should.strictEqual(err, null);
client2.set('c', 'd', function (err) {
should.strictEqual(err, null);
client1.has('a', function (err, val) {
should.strictEqual(err, null);
val.should.be.true;
client2.has('c', function (err, val) {
should.strictEqual(err, null);
val.should.be.true;
store.clients.should.have.property(rand1);
store.clients.should.have.property(rand2);
store.destroyClient(rand1);
store.clients.should.not.have.property(rand1);
store.clients.should.have.property(rand2);
client1.has('a', function (err, val) {
should.strictEqual(err, null);
val.should.equal(false);
store.destroy();
done();
});
});
});
});
});
},
'test destroy expiration': function (done) {
var store = new MemoryStore()
, id = Math.abs(Math.random() * Date.now() | 0)
, client = store.client(id);
client.set('a', 'b', function (err) {
should.strictEqual(err, null);
store.destroyClient(id, 1);
setTimeout(function () {
client.get('a', function (err, val) {
should.strictEqual(err, null);
val.should.equal('b');
});
}, 500);
setTimeout(function () {
client.get('a', function (err, val) {
should.strictEqual(err, null);
should.strictEqual(val, null);
store.destroy();
done();
});
}, 1900);
});
}
};

240
test/stores.redis.test.js Normal file
View File

@@ -0,0 +1,240 @@
/**
* Test dependencies
*
* @api private
*/
var sio = require('../')
, redis = require('redis')
, should = require('should')
, RedisStore = sio.RedisStore;
/**
* Test.
*/
module.exports = {
'test publishing doesnt get caught by the own store subscriber': function (done) {
var a = new RedisStore
, b = new RedisStore;
a.subscribe('woot', function (arg) {
arg.should.equal('bb');
a.destroy();
b.destroy();
done();
}, function () {
a.publish('woot', 'aa');
b.publish('woot', 'bb');
});
},
'test publishing to multiple subscribers': function (done) {
var a = new RedisStore
, b = new RedisStore
, c = new RedisStore
, subscriptions = 3
, messages = 2;
a.subscribe('tobi', function () {
throw new Error('Shouldnt publish to itself');
}, publish);
function subscription (arg1, arg2, arg3) {
arg1.should.equal(1);
arg2.should.equal(2);
arg3.should.equal(3);
--messages || finish();
}
b.subscribe('tobi', subscription, publish);
c.subscribe('tobi', subscription, publish);
function publish () {
--subscriptions || a.publish('tobi', 1, 2, 3);
}
function finish () {
a.destroy();
b.destroy();
c.destroy();
done();
}
},
'test storing data for a client': function (done) {
var store = new RedisStore
, rand = 'test-' + Date.now()
, client = store.client(rand);
client.id.should.equal(rand);
client.set('a', 'b', function (err) {
should.strictEqual(err, null);
client.get('a', function (err, val) {
should.strictEqual(err, null);
val.should.equal('b');
client.has('a', function (err, has) {
should.strictEqual(err, null);
has.should.be.true;
client.has('b', function (err, has) {
should.strictEqual(err, null);
has.should.be.false;
client.del('a', function (err) {
should.strictEqual(err, null);
client.has('a', function (err, has) {
should.strictEqual(err, null);
has.should.be.false;
client.set('b', 'c', function (err) {
should.strictEqual(err, null);
client.set('c', 'd', function (err) {
should.strictEqual(err, null);
client.get('b', function (err, val) {
should.strictEqual(err, null);
val.should.equal('c');
client.get('c', function (err, val) {
should.strictEqual(err, null);
val.should.equal('d');
store.destroy();
done();
});
});
});
});
});
});
});
});
});
});
},
'test cleaning up clients data': function (done) {
var rand1 = Math.abs(Math.random() * Date.now() | 0)
, rand2 = Math.abs(Math.random() * Date.now() | 0);
var store = new RedisStore()
, client1 = store.client(rand1)
, client2 = store.client(rand2);
client1.set('a', 'b', function (err) {
should.strictEqual(err, null);
client2.set('c', 'd', function (err) {
should.strictEqual(err, null);
client1.has('a', function (err, val) {
should.strictEqual(err, null);
val.should.be.true;
client2.has('c', function (err, val) {
should.strictEqual(err, null);
val.should.be.true;
store.destroy();
var newstore = new RedisStore()
, newclient1 = newstore.client(rand1)
, newclient2 = newstore.client(rand2);
newclient1.has('a', function (err, val) {
should.strictEqual(err, null);
val.should.be.false;
newclient2.has('c', function (err, val) {
should.strictEqual(err, null);
val.should.be.false;
newstore.destroy();
done();
});
});
});
});
});
});
},
'test cleaning up a particular client': function (done) {
var rand1 = Math.abs(Math.random() * Date.now() | 0)
, rand2 = Math.abs(Math.random() * Date.now() | 0);
var store = new RedisStore()
, client1 = store.client(rand1)
, client2 = store.client(rand2);
client1.set('a', 'b', function (err) {
should.strictEqual(err, null);
client2.set('c', 'd', function (err) {
should.strictEqual(err, null);
client1.has('a', function (err, val) {
should.strictEqual(err, null);
val.should.be.true;
client2.has('c', function (err, val) {
should.strictEqual(err, null);
val.should.be.true;
store.clients.should.have.property(rand1);
store.clients.should.have.property(rand2);
store.destroyClient(rand1);
store.clients.should.not.have.property(rand1);
store.clients.should.have.property(rand2);
client1.has('a', function (err, val) {
should.strictEqual(err, null);
val.should.equal(false);
store.destroy();
done();
});
});
});
});
});
},
'test destroy expiration': function (done) {
var store = new RedisStore()
, id = Math.abs(Math.random() * Date.now() | 0)
, client = store.client(id);
client.set('a', 'b', function (err) {
should.strictEqual(err, null);
store.destroyClient(id, 1);
setTimeout(function () {
client.get('a', function (err, val) {
should.strictEqual(err, null);
val.should.equal('b');
});
}, 500);
setTimeout(function () {
client.get('a', function (err, val) {
should.strictEqual(err, null);
should.strictEqual(val, null);
store.destroy();
done();
});
}, 2000);
});
}
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -0,0 +1,187 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Test dependencies.
*/
var sio = require('../')
, net = require('net')
, http = require('http')
, should = require('./common')
, WebSocket = require('../support/node-websocket-client/lib/websocket').WebSocket
, WSClient = require('./transports.websocket.test')
, parser = sio.parser
, ports = 15600;
/**
* FlashSocket client constructor.
*
* @api private
*/
function FlashSocket (port, sid) {
this.sid = sid;
this.port = port;
WebSocket.call(
this
, 'ws://localhost:' + port + '/socket.io/'
+ sio.protocol + '/flashsocket/' + sid
);
};
/**
* Inherits from WSClient.
*/
FlashSocket.prototype.__proto__ = WebSocket.prototype;
/**
* Creates a TCP connection to a port.
*
* @api public
*/
function netConnection (port, callback){
var nclient = net.createConnection(port);
nclient.on('data', function (data) {
callback.call(nclient, null, data);
});
nclient.on('error', function (e){
callback.call(nclient, e);
});
nclient.write('<policy-file-request/>\0');
}
/**
* Tests.
*/
module.exports = {
'flashsocket disabled by default': function (done) {
var io = sio.listen(http.createServer());
io.get('transports').should.not.contain('flashsocket');
done();
},
'flash policy port': function (done) {
var io = sio.listen(http.createServer())
, port = ++ports;
io.get('flash policy port').should.eql(10843);
io.set('flash policy port', port);
io.get('flash policy port').should.eql(port);
should.strictEqual(io.flashPolicyServer, undefined);
netConnection(port, function (err, data){
err.should.be.an.instanceof(Error);
err.code.should.eql('ECONNREFUSED');
this.destroy();
done();
})
},
'start flash policy': function (done) {
var io = sio.listen(http.createServer())
, port = ++ports;
io.set('flash policy port', port);
io.set('transports', ['flashsocket']);
io.flashPolicyServer.should.be.a('object');
netConnection(port, function (err, data){
should.strictEqual(err, null);
data.toString().should.match(/<cross-domain-policy>/);
this.destroy();
io.flashPolicyServer.close();
done();
})
},
'change running flash server port': function (done) {
var io = sio.listen(http.createServer())
, port = ++ports
, next = ++ports;
io.set('flash policy port', port);
io.set('transports', ['flashsocket']);
io.set('flash policy port', next);
io.flashPolicyServer.port.should.eql(next);
netConnection(port, function (err, data){
err.should.be.an.instanceof(Error);
err.code.should.eql('ECONNREFUSED');
this.destroy();
// should work
netConnection(next, function (err, data){
should.strictEqual(err, null);
data.toString().should.match(/<cross-domain-policy>/);
this.destroy();
io.flashPolicyServer.close();
done();
});
});
},
'different origins': function(done) {
var io = sio.listen(http.createServer())
, port = ++ports;
io.set('flash policy port', port);
io.set('transports', ['flashsocket']);
io.set('origins', 'google.com:80');
var server = io.flashPolicyServer;
server.origins.should.contain('google.com:80');
server.origins.should.not.contain('*.*');
io.set('origins', ['foo.bar:80', 'socket.io:1337']);
server.origins.should.not.contain('google.com:80');
server.origins.should.contain('foo.bar:80');
server.origins.should.contain('socket.io:1337');
server.buffer.toString('utf8').should.match(/socket\.io/);
io.flashPolicyServer.close();
done();
},
'flashsocket identifies as flashsocket': function (done) {
var cl = client(++ports)
, io = create(cl)
, ws;
io.set('transports', ['flashsocket']);
io.sockets.on('connection', function (socket) {
socket.manager.transports[socket.id].name.should.equal('flashsocket');
ws.finishClose();
cl.end();
io.flashPolicyServer.close();
io.server.close();
done();
});
cl.handshake(function (sid) {
ws = websocket(cl, sid, 'flashsocket');
});
}
};

View File

@@ -0,0 +1,458 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Test dependencies.
*/
var sio = require('../')
, should = require('./common')
, HTTPClient = should.HTTPClient
, parser = sio.parser
, ports = 15300;
/**
* HTTPClient for htmlfile transport.
*/
function HTMLFile (port) {
HTTPClient.call(this, port);
};
/**
* Inhertis from HTTPClient.
*/
HTMLFile.prototype.__proto__ = HTTPClient.prototype;
/**
* Override GET request with streaming parser.
*
* @api public
*/
var head = '<script>_('
, foot = ');</script>'
, initial = '<html><body>'
+ '<script>var _ = function (msg) { parent.s._(msg, document); };</script>'
+ new Array(174).join(' ')
HTMLFile.prototype.data = function (path, opts, fn) {
if ('function' == typeof opts) {
fn = opts;
opts = {};
}
opts.buffer = false;
return this.request(path, opts, function (res) {
var buf = ''
, messages = 0
, state = 0;
res.on('data', function (chunk) {
buf += chunk;
function parse () {
switch (state) {
case 0:
if (buf.indexOf(initial) === 0) {
buf = buf.substr(initial.length);
state = 1;
} else {
break;
}
case 1:
if (buf.indexOf(head) === 0) {
buf = buf.substr(head.length);
state = 2;
} else {
break;
}
case 2:
if (buf.indexOf(foot) != -1) {
var data = buf.slice(0, buf.indexOf(foot))
, obj = JSON.parse(data);
fn(obj === '' ? obj : parser.decodePayload(obj), ++messages);
buf = buf.substr(data.length + foot.length);
state = 1;
parse();
}
};
};
parse();
});
});
};
/**
* Create client for this transport.
*
* @api public
*/
function client (port) {
return new HTMLFile(port);
};
/**
* Tests.
*/
module.exports = {
'test that not responding to a heartbeat drops client': function (done) {
var port = ++ports
, cl = client(port)
, io = create(cl)
, beat = false;
io.configure(function () {
io.set('heartbeat interval', .05);
io.set('heartbeat timeout', .05);
io.set('close timeout', 0);
});
io.sockets.on('connection', function (socket) {
socket.on('disconnect', function (reason) {
beat.should.be.true;
reason.should.eql('heartbeat timeout');
cl.end();
io.server.close();
done();
});
});
cl.handshake(function (sid) {
cl.data('/socket.io/{protocol}/htmlfile/' + sid, function (msgs, i) {
switch (i) {
case 1:
msgs.should.have.length(1);
msgs[0].type.should.eql('connect');
msgs[0].endpoint.should.eql('');
break;
case 2:
msgs.should.have.length(1);
msgs[0].type.should.eql('heartbeat');
beat = true;
};
});
});
},
'test that responding to a heartbeat maintains session': function (done) {
var port = ++ports
, cl = client(port)
, io = create(cl)
, heartbeats = 0;
io.configure(function () {
io.set('heartbeat interval', .05);
io.set('heartbeat timeout', .05);
io.set('close timeout', 0);
});
io.sockets.on('connection', function (socket) {
socket.on('disconnect', function (reason) {
heartbeats.should.eql(2);
reason.should.eql('heartbeat timeout');
cl.end();
io.server.close();
done();
});
});
cl.handshake(function (sid) {
cl.data('/socket.io/{protocol}/htmlfile/' + sid, function (msgs, i) {
switch (i) {
case 1:
msgs.should.have.length(1);
msgs[0].type.should.eql('connect');
msgs[0].endpoint.should.eql('');
break;
default:
msgs.should.have.length(1);
msgs[0].type.should.eql('heartbeat');
heartbeats++;
if (heartbeats == 1) {
cl.post('/socket.io/{protocol}/htmlfile/' + sid, parser.encodePacket({
type: 'heartbeat'
}));
}
}
});
});
},
'test sending undeliverable volatile messages': function (done) {
var port = ++ports
, cl = client(port)
, io = create(cl)
, messaged = false
, s;
io.configure(function () {
io.set('close timeout', 0);
});
io.sockets.on('connection', function (socket) {
s = socket;
socket.on('disconnect', function () {
messaged.should.be.false;
io.server.close();
done();
});
});
cl.handshake(function (sid) {
cl.data('/socket.io/{protocol}/htmlfile/' + sid, function () { });
setTimeout(function () {
cl.end();
setTimeout(function () {
s.volatile.send('wooooot');
cl = client(port);
cl.data('/socket.io/{protocol}/htmlfile/' + sid, function (msgs) {
if (msgs && msgs.length)
messaged = true;
});
setTimeout(function () {
cl.end();
}, 20);
}, 20);
}, 20);
});
},
'test sending undeliverable volatile json': function (done) {
var port = ++ports
, cl = client(port)
, io = create(cl)
, messaged = false
, s;
io.configure(function () {
io.set('close timeout', 0);
});
io.sockets.on('connection', function (socket) {
s = socket;
socket.on('disconnect', function () {
messaged.should.be.false;
io.server.close();
done();
});
});
cl.handshake(function (sid) {
cl.data('/socket.io/{protocol}/htmlfile/' + sid, function () { });
setTimeout(function () {
cl.end();
setTimeout(function () {
s.volatile.json.send(123);
cl = client(port);
cl.data('/socket.io/{protocol}/htmlfile/' + sid, function (msgs) {
if (msgs && msgs.length)
messaged = true;
});
setTimeout(function () {
cl.end();
}, 20);
}, 20);
}, 20);
});
},
'test sending undeliverable volatile events': function (done) {
var port = ++ports
, cl = client(port)
, io = create(cl)
, messaged = false
, s;
io.configure(function () {
io.set('close timeout', 0);
});
io.sockets.on('connection', function (socket) {
s = socket;
socket.on('disconnect', function () {
messaged.should.be.false;
io.server.close();
done();
});
});
cl.handshake(function (sid) {
cl.data('/socket.io/{protocol}/htmlfile/' + sid, function () { });
setTimeout(function () {
cl.end();
setTimeout(function () {
s.volatile.emit('tobi');
cl = client(port);
cl.data('/socket.io/{protocol}/htmlfile/' + sid, function (msgs) {
if (msgs && msgs.length)
messaged = true;
});
setTimeout(function () {
cl.end();
}, 20);
}, 20);
}, 20);
});
},
'test sending deliverable volatile messages': function (done) {
var port = ++ports
, cl = client(port)
, io = create(cl)
, messaged = false;
io.configure(function () {
io.set('close timeout', 0);
});
io.sockets.on('connection', function (socket) {
socket.volatile.send('woot');
socket.on('disconnect', function () {
io.server.close();
done();
});
});
cl.handshake(function (sid) {
cl.data('/socket.io/{protocol}/htmlfile/' + sid, function (msgs, i) {
switch (i) {
case 1:
msgs.should.have.length(1);
msgs[0].type.should.eql('connect');
msgs[0].endpoint.should.eql('');
break;
case 2:
msgs.should.have.length(1);
msgs[0].should.eql({
type: 'message'
, data: 'woot'
, endpoint: ''
});
cl.end();
}
});
});
},
'test sending deliverable volatile json': function (done) {
var port = ++ports
, cl = client(port)
, io = create(cl)
, messaged = false;
io.configure(function () {
io.set('close timeout', 0);
});
io.sockets.on('connection', function (socket) {
socket.volatile.json.send(['woot']);
socket.on('disconnect', function () {
io.server.close();
done();
});
});
cl.handshake(function (sid) {
cl.data('/socket.io/{protocol}/htmlfile/' + sid, function (msgs, i) {
switch (i) {
case 1:
msgs.should.have.length(1);
msgs[0].type.should.eql('connect');
msgs[0].endpoint.should.eql('');
break;
case 2:
msgs.should.have.length(1);
msgs[0].should.eql({
type: 'json'
, data: ['woot']
, endpoint: ''
});
cl.end();
}
});
});
},
'test sending deliverable volatile events': function (done) {
var port = ++ports
, cl = client(port)
, io = create(cl)
, messaged = false;
io.configure(function () {
io.set('close timeout', 0);
});
io.sockets.on('connection', function (socket) {
socket.volatile.emit('aaa');
socket.on('disconnect', function () {
io.server.close();
done();
});
});
cl.handshake(function (sid) {
cl.data('/socket.io/{protocol}/htmlfile/' + sid, function (msgs, i) {
switch (i) {
case 1:
msgs.should.have.length(1);
msgs[0].type.should.eql('connect');
msgs[0].endpoint.should.eql('');
break;
case 2:
msgs.should.have.length(1);
msgs[0].should.eql({
type: 'event'
, name: 'aaa'
, endpoint: ''
, args: []
});
cl.end();
}
});
});
}
};

View File

@@ -0,0 +1,774 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Test dependencies.
*/
var sio = require('../')
, should = require('./common')
, qs = require('querystring')
, HTTPClient = should.HTTPClient
, parser = sio.parser
, ports = 15500;
/**
* HTTPClient for jsonp-polling transport.
*/
function JSONPPolling (port) {
HTTPClient.call(this, port);
};
/**
* Inhertis from HTTPClient.
*/
JSONPPolling.prototype.__proto__ = HTTPClient.prototype;
/**
* Performs a json-p (cross domain) handshake
*
* @api public
*/
JSONPPolling.prototype.handshake = function (opts, fn) {
if ('function' == typeof opts) {
fn = opts;
opts = {};
}
var self = this;
return this.get(
'/socket.io/{protocol}?jsonp=0'
, opts
, function (res, data) {
var head = 'io.j[0]('
, foot = ');';
data.substr(0, head.length).should.eql(head);
data.substr(-foot.length).should.eql(foot);
data = data.slice(head.length, data.length - foot.length);
var parts = JSON.parse(data).split(':');
if (opts.ignoreConnect) {
return fn && fn.apply(null, parts);
}
// expect connect packet right after handshake
self.get(
'/socket.io/{protocol}/jsonp-polling/' + parts[0]
, function (res, msgs) {
res.statusCode.should.eql(200);
msgs.should.have.length(1);
msgs[0].should.eql({ type: 'connect', endpoint: '', qs: '' });
fn && fn.apply(null, parts);
}
);
}
);
};
/**
* Override GET requests.
*
* @api public
*/
JSONPPolling.prototype.get = function (path, opts, fn) {
if ('function' == typeof opts) {
fn = opts;
opts = {};
}
opts = opts || {};
opts.parse = function (data) {
var head = 'io.j[0]('
, foot = ');';
if (~path.indexOf('?i=1')) {
head = 'io.j[1](';
}
data.substr(0, head.length).should.eql(head);
data.substr(-foot.length).should.eql(foot);
data = data.substr(head.length, data.length - head.length - foot.length);
return JSON.parse(data);
};
return HTTPClient.prototype.get.call(this, path, opts, fn);
};
/**
* Issue an encoded POST request
*
* @api private
*/
JSONPPolling.prototype.post = function (path, data, opts, fn) {
if ('function' == typeof opts) {
fn = opts;
opts = {};
}
opts = opts || {};
opts.method = 'POST';
opts.data = qs.stringify({ d: data });
return this.request(path, opts, fn);
};
/**
* Create client for this transport.
*
* @api public
*/
function client (port) {
return new JSONPPolling(port);
};
/**
* Test.
*/
module.exports = {
'test jsonp handshake': function (done) {
var cl = client(++ports)
, io = create(cl);
io.configure(function () {
io.set('close timeout', .05);
io.set('polling duration', 0);
});
function finish () {
cl.end();
io.server.close();
done();
};
cl.handshake(function (sid) {
var total = 2;
cl.get('/socket.io/{protocol}/jsonp-polling/tobi', function (res, msgs) {
res.statusCode.should.eql(200);
msgs.should.have.length(1);
msgs[0].should.eql({
type: 'error'
, reason: 'client not handshaken'
, endpoint: ''
, advice: 'reconnect'
});
--total || finish();
});
cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
res.statusCode.should.eql(200);
msgs.should.have.length(1);
msgs[0].should.eql({ type: 'noop', endpoint: '' });
--total || finish();
});
});
},
'test the connection event': function (done) {
var cl = client(++ports)
, io = create(cl)
, sid;
io.configure(function () {
io.set('polling duration', 0);
io.set('close timeout', .05);
});
io.sockets.on('connection', function (socket) {
socket.id.should.eql(sid);
socket.on('disconnect', function () {
cl.end();
io.server.close();
done();
});
});
cl.handshake({ ignoreConnect: true }, function (sessid) {
sid = sessid;
cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
res.statusCode.should.eql(200);
msgs.should.have.length(1);
msgs[0].type.should.eql('connect');
});
});
},
'test the disconnection event after a close timeout': function (done) {
var cl = client(++ports)
, io = create(cl)
, sid;
io.configure(function () {
io.set('close timeout', 0);
});
io.sockets.on('connection', function (socket) {
socket.id.should.eql(sid);
socket.on('disconnect', function () {
io.server.close();
done();
});
});
cl.handshake({ ignoreConnect: true }, function (sessid) {
sid = sessid;
cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
res.statusCode.should.eql(200);
msgs.should.have.length(1);
msgs[0].type.should.eql('connect');
setTimeout(function () {
cl.end();
}, 10);
});
});
},
'test the disconnection event when the client sends ?disconnect req': function (done) {
var cl = client(++ports)
, io = create(cl)
, disconnected = false
, sid;
io.configure(function () {
io.set('close timeout', .1);
});
io.sockets.on('connection', function (socket) {
socket.on('disconnect', function () {
disconnected = true;
});
});
cl.handshake({ ignoreConnect: true }, function (sessid) {
sid = sessid;
cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
res.statusCode.should.eql(200);
msgs.should.have.length(1);
msgs[0].type.should.eql('connect');
cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
msgs.should.have.length(1);
msgs[0].should.eql({ type: 'disconnect', endpoint: '' });
disconnected.should.be.true;
io.server.close();
cl.end();
done();
});
// with the new http bits in node 0.5, there's no guarantee that
// the previous request is actually dispatched (and received) before the following
// reset call is sent. to not waste more time on a workaround, a timeout is added.
setTimeout(function() {
cl.get('/socket.io/{protocol}/jsonp-polling/' + sid + '/?disconnect');
}, 500);
});
});
},
'test the disconnection event booting a client': function (done) {
var cl = client(++ports)
, io = create(cl)
, forced = false;
io.sockets.on('connection', function (socket) {
socket.on('disconnect', function () {
io.server.close();
done();
});
cl.end();
socket.disconnect();
forced = true;
});
cl.handshake(function (sid) {
cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
msgs.should.have.length(1);
msgs[0].should.eql({ type: 'disconnect', endpoint: '' });
forced.should.be.true;
});
});
},
'test the disconnection event with client disconnect packet': function (done) {
var cl = client(++ports)
, io = create(cl)
, sid;
io.sockets.on('connection', function (client) {
cl.post(
'/socket.io/{protocol}/jsonp-polling/' + sid
, JSON.stringify(parser.encodePacket({ type: 'disconnect' }))
, function (res, data) {
res.statusCode.should.eql(200);
data.should.eql('1');
}
);
client.on('disconnect', function () {
cl.end();
io.server.close();
done();
});
});
cl.handshake({ ignoreConnect: true }, function (sessid) {
sid = sessid;
cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
res.statusCode.should.eql(200);
msgs.should.have.length(1);
msgs[0].type.should.eql('connect');
});
});
},
'test sending back data': function (done) {
var cl = client(++ports)
, io = create(cl);
io.configure(function () {
io.set('polling duration', .05);
io.set('close timeout', .05);
});
io.sockets.on('connection', function (socket) {
socket.send('woot');
socket.on('disconnect', function () {
cl.end();
io.server.close();
done();
});
});
cl.handshake(function (sid) {
cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, packs) {
packs.should.have.length(1);
packs[0].type.should.eql('message');
packs[0].data.should.eql('woot');
});
});
},
'test sending a batch of messages': function (done) {
var cl = client(++ports)
, io = create(cl)
, sid;
io.configure(function () {
io.set('close timeout', .05);
});
io.sockets.on('connection', function (socket) {
var messages = 0;
cl.post(
'/socket.io/{protocol}/jsonp-polling/' + sid
, JSON.stringify(parser.encodePayload([
parser.encodePacket({ type: 'message', data: 'a' })
, parser.encodePacket({ type: 'message', data: 'b' })
, parser.encodePacket({ type: 'disconnect' })
]))
, function (res, data) {
res.statusCode.should.eql(200);
data.should.eql('1');
}
);
socket.on('message', function (data) {
messages++;
if (messages == 1)
data.should.eql('a');
if (messages == 2)
data.should.eql('b');
});
socket.on('disconnect', function () {
messages.should.eql(2);
cl.end();
io.server.close();
done();
});
});
cl.handshake({ ignoreConnect: true }, function (sessid) {
sid = sessid;
cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
res.statusCode.should.eql(200);
msgs.should.have.length(1);
msgs[0].type.should.eql('connect');
});
});
},
'test message buffering between a response and a request': function (done) {
var cl = client(++ports)
, io = create(cl)
, messages = false
, tobi;
io.configure(function () {
io.set('polling duration', .1);
io.set('close timeout', .2);
});
io.sockets.on('connection', function (socket) {
tobi = function () {
socket.send('a');
socket.send('b');
socket.send('c');
};
socket.on('disconnect', function () {
messages.should.be.true;
cl.end();
io.server.close();
done();
});
});
cl.handshake(function (sid) {
cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
msgs.should.have.length(1);
msgs[0].should.eql({ type: 'noop', endpoint: '' });
tobi();
cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
msgs.should.have.length(3);
msgs[0].should.eql({ type: 'message', endpoint: '', data: 'a' });
msgs[1].should.eql({ type: 'message', endpoint: '', data: 'b' });
msgs[2].should.eql({ type: 'message', endpoint: '', data: 'c' });
messages = true;
});
})
});
},
'test connecting to a specific endpoint': function (done) {
var cl = client(++ports)
, io = create(cl)
, connectMessage = false
, sid;
io.configure(function () {
io.set('polling duration', 0);
io.set('close timeout', .05);
});
io.of('/woot').on('connection', function (socket) {
connectMessage.should.be.true;
socket.on('disconnect', function () {
cl.end();
io.server.close();
done();
});
});
cl.handshake(function (sid) {
cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, data) {
cl.get('/socket.io/{protocol}/jsonp-polling/' + sid);
connectMessage = true;
cl.post(
'/socket.io/{protocol}/jsonp-polling/' + sid
, JSON.stringify(parser.encodePacket({ type: 'connect', endpoint: '/woot' }))
, function (res, data) {
res.statusCode.should.eql(200);
data.should.eql('1');
}
);
});
});
},
'test that connecting doesnt connect to defined endpoints': function (done) {
var cl = client(++ports)
, io = create(cl)
, tobiConnected = false
, mainConnected = false
, sid;
io.configure(function () {
io.set('polling duration', .05);
io.set('close timeout', .05);
});
io.sockets.on('connection', function (socket) {
mainConnected = true;
socket.on('disconnect', function () {
tobiConnected.should.be.false;
cl.end();
io.server.close();
done();
});
});
io.of('/tobi').on('connection', function () {
tobiConnected = true;
});
cl.handshake(function (sid) {
cl.get('/socket.io/{protocol}/jsonp-polling/' + sid);
});
},
'test disconnecting a specific endpoint': function (done) {
var cl = client(++ports)
, io = create(cl)
, wootDisconnected = false
, mainDisconnected = false
, checked = false;
io.configure(function () {
io.set('polling duration', 0);
io.set('close timeout', .05);
});
io.sockets.on('connection', function (socket) {
socket.on('message', function (data) {
data.should.eql('ferret');
mainDisconnected.should.be.false;
wootDisconnected.should.be.true;
checked = true;
});
socket.on('disconnect', function () {
mainDisconnected = true;
checked.should.be.true;
cl.end();
io.server.close();
done();
});
});
io.of('/woot').on('connection', function (socket) {
socket.on('disconnect', function () {
wootDisconnected = true;
});
});
cl.handshake(function (sid) {
cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function () {
cl.post(
'/socket.io/{protocol}/jsonp-polling/' + sid
, JSON.stringify(parser.encodePacket({ type: 'connect', endpoint: '/woot' }))
, function (res, data) {
res.statusCode.should.eql(200);
data.should.eql('1');
cl.post(
'/socket.io/{protocol}/jsonp-polling/' + sid
, JSON.stringify(parser.encodePacket({ type: 'disconnect', endpoint: '/woot' }))
, function (res, data) {
res.statusCode.should.eql(200);
data.should.eql('1');
cl.post(
'/socket.io/{protocol}/jsonp-polling/' + sid
, JSON.stringify(parser.encodePacket({ type: 'message', data: 'ferret' }))
, function (res, data) {
res.statusCode.should.eql(200);
data.should.eql('1');
}
);
}
);
}
);
});
});
},
'test that disconnecting disconnects all endpoints': function (done) {
var cl = client(++ports)
, io = create(cl)
, aDisconnected = false
, bDisconnected = false;
io.configure(function () {
io.set('polling duration', .2);
io.set('close timeout', .5);
});
io.sockets.on('connection', function (socket) {
socket.on('disconnect', function () {
setTimeout(function () {
aDisconnected.should.be.true;
bDisconnected.should.be.true;
cl.end();
io.server.close();
done();
}, 50);
});
});
io.of('/a').on('connection', function (socket) {
socket.on('disconnect', function (msg) {
aDisconnected = true;
});
});
io.of('/b').on('connection', function (socket) {
socket.on('disconnect', function (msg) {
bDisconnected = true;
});
});
cl.handshake(function (sid) {
cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
res.statusCode.should.eql(200);
msgs.should.have.length(1);
msgs[0].should.eql({ type: 'noop', endpoint: '' });
cl.post(
'/socket.io/{protocol}/jsonp-polling/' + sid
, JSON.stringify(parser.encodePacket({ type: 'connect', endpoint: '/a' }))
, function (res, data) {
res.statusCode.should.eql(200);
data.should.eql('1');
}
);
cl.post(
'/socket.io/{protocol}/jsonp-polling/' + sid
, JSON.stringify(parser.encodePacket({ type: 'connect', endpoint: '/b' }))
, function (res, data) {
res.statusCode.should.eql(200);
data.should.eql('1');
}
);
});
});
},
'test messaging a specific endpoint': function (done) {
var cl = client(++ports)
, io = create(cl)
, messaged = true
, aMessaged = false
, bMessaged = false;
io.configure(function () {
io.set('polling duration', 0);
io.set('close timeout', .05);
});
io.sockets.on('connection', function (socket) {
socket.on('message', function (msg) {
msg.should.eql('');
messaged = true;
});
socket.on('disconnect', function () {
messaged.should.be.true;
aMessaged.should.be.true;
bMessaged.should.be.true;
cl.end();
io.server.close();
done();
});
});
io.of('/a').on('connection', function (socket) {
socket.on('message', function (msg) {
msg.should.eql('a');
aMessaged = true;
});
});
io.of('/b').on('connection', function (socket) {
socket.on('message', function (msg) {
msg.should.eql('b');
bMessaged = true;
});
});
cl.handshake(function (sid) {
cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, data) {
cl.post(
'/socket.io/{protocol}/jsonp-polling/' + sid
, JSON.stringify(parser.encodePacket({ type: 'message', data: '' }))
, function (res, data) {
res.statusCode.should.eql(200);
data.should.eql('1');
}
);
cl.post(
'/socket.io/{protocol}/jsonp-polling/' + sid
, JSON.stringify(parser.encodePacket({ type: 'connect', endpoint: '/a' }))
, function (res, data) {
res.statusCode.should.eql(200);
data.should.eql('1');
cl.post(
'/socket.io/{protocol}/jsonp-polling/' + sid
, JSON.stringify(parser.encodePacket({ type: 'message', endpoint: '/a', data: 'a' }))
, function (res, data) {
res.statusCode.should.eql(200);
data.should.eql('1');
}
);
}
);
cl.post(
'/socket.io/{protocol}/jsonp-polling/' + sid
, JSON.stringify(parser.encodePacket({ type: 'connect', endpoint: '/b' }))
, function (res, data) {
res.statusCode.should.eql(200);
data.should.eql('1');
cl.post(
'/socket.io/{protocol}/jsonp-polling/' + sid
, JSON.stringify(parser.encodePacket({ type: 'message', endpoint: '/b', data: 'b' }))
, function (res, data) {
res.statusCode.should.eql(200);
data.should.eql('1');
}
);
}
);
});
});
}
};

View File

@@ -0,0 +1,262 @@
/**
* Test dependencies.
*/
var assert = require('assert');
var Parser = require('../lib/transports/websocket/hybi-07-12.js').Parser;
require('./hybi-common');
/**
* Tests.
*/
module.exports = {
'can parse unmasked text message': function() {
var p = new Parser();
var packet = '81 05 48 65 6c 6c 6f';
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal('Hello', data);
});
p.add(getBufferFromHexString(packet));
assert.ok(gotData);
},
'can parse close message': function() {
var p = new Parser();
var packet = '88 00';
var gotClose = false;
p.on('close', function(data) {
gotClose = true;
});
p.add(getBufferFromHexString(packet));
assert.ok(gotClose);
},
'can parse masked text message': function() {
var p = new Parser();
var packet = '81 93 34 83 a8 68 01 b9 92 52 4f a1 c6 09 59 e6 8a 52 16 e6 cb 00 5b a1 d5';
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal('5:::{"name":"echo"}', data);
});
p.add(getBufferFromHexString(packet));
assert.ok(gotData);
},
'can parse a masked text message longer than 125 bytes': function() {
var p = new Parser();
var message = 'A';
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
var packet = '81 FE ' + pack(4, message.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal(message, data);
});
p.add(getBufferFromHexString(packet));
assert.ok(gotData);
},
'can parse a really long masked text message': function() {
var p = new Parser();
var message = 'A';
for (var i = 0; i < 64*1024; ++i) message += (i % 5).toString();
var packet = '81 FF ' + pack(16, message.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal(message, data);
});
p.add(getBufferFromHexString(packet));
assert.ok(gotData);
},
'can parse a fragmented masked text message of 300 bytes': function() {
var p = new Parser();
var message = 'A';
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
var msgpiece1 = message.substr(0, 150);
var msgpiece2 = message.substr(150);
var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece1, '34 83 a8 68'));
var packet2 = '80 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece2, '34 83 a8 68'));
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal(message, data);
});
p.add(getBufferFromHexString(packet1));
p.add(getBufferFromHexString(packet2));
assert.ok(gotData);
},
'can parse a ping message': function() {
var p = new Parser();
var message = 'Hello';
var packet = '89 FE ' + pack(4, message.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
var gotPing = false;
p.on('ping', function(data) {
gotPing = true;
assert.equal(message, data);
});
p.add(getBufferFromHexString(packet));
assert.ok(gotPing);
},
'can parse a ping with no data': function() {
var p = new Parser();
var packet = '89 00';
var gotPing = false;
p.on('ping', function(data) {
gotPing = true;
});
p.add(getBufferFromHexString(packet));
assert.ok(gotPing);
},
'can parse a fragmented masked text message of 300 bytes with a ping in the middle': function() {
var p = new Parser();
var message = 'A';
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
var msgpiece1 = message.substr(0, 150);
var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece1, '34 83 a8 68'));
var pingMessage = 'Hello';
var pingPacket = '89 FE ' + pack(4, pingMessage.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(pingMessage, '34 83 a8 68'));
var msgpiece2 = message.substr(150);
var packet2 = '80 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece2, '34 83 a8 68'));
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal(message, data);
});
var gotPing = false;
p.on('ping', function(data) {
gotPing = true;
assert.equal(pingMessage, data);
});
p.add(getBufferFromHexString(packet1));
p.add(getBufferFromHexString(pingPacket));
p.add(getBufferFromHexString(packet2));
assert.ok(gotData);
assert.ok(gotPing);
},
'can parse a fragmented masked text message of 300 bytes with a ping in the middle, which is delievered over sevaral tcp packets': function() {
var p = new Parser();
var message = 'A';
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
var msgpiece1 = message.substr(0, 150);
var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece1, '34 83 a8 68'));
var pingMessage = 'Hello';
var pingPacket = '89 FE ' + pack(4, pingMessage.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(pingMessage, '34 83 a8 68'));
var msgpiece2 = message.substr(150);
var packet2 = '80 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece2, '34 83 a8 68'));
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal(message, data);
});
var gotPing = false;
p.on('ping', function(data) {
gotPing = true;
assert.equal(pingMessage, data);
});
var buffers = [];
buffers = buffers.concat(splitBuffer(getBufferFromHexString(packet1)));
buffers = buffers.concat(splitBuffer(getBufferFromHexString(pingPacket)));
buffers = buffers.concat(splitBuffer(getBufferFromHexString(packet2)));
for (var i = 0; i < buffers.length; ++i) {
p.add(buffers[i]);
}
assert.ok(gotData);
assert.ok(gotPing);
},
'can parse a 100 byte long masked binary message': function() {
var p = new Parser();
var length = 100;
var message = new Buffer(length);
for (var i = 0; i < length; ++i) message[i] = i % 256;
var originalMessage = getHexStringFromBuffer(message);
var packet = '82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
var gotData = false;
p.on('binary', function(data) {
gotData = true;
assert.equal(originalMessage, getHexStringFromBuffer(data));
});
p.add(getBufferFromHexString(packet));
assert.ok(gotData);
},
'can parse a 256 byte long masked binary message': function() {
var p = new Parser();
var length = 256;
var message = new Buffer(length);
for (var i = 0; i < length; ++i) message[i] = i % 256;
var originalMessage = getHexStringFromBuffer(message);
var packet = '82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
var gotData = false;
p.on('binary', function(data) {
gotData = true;
assert.equal(originalMessage, getHexStringFromBuffer(data));
});
p.add(getBufferFromHexString(packet));
assert.ok(gotData);
},
'can parse a 200kb long masked binary message': function() {
var p = new Parser();
var length = 200 * 1024;
var message = new Buffer(length);
for (var i = 0; i < length; ++i) message[i] = i % 256;
var originalMessage = getHexStringFromBuffer(message);
var packet = '82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
var gotData = false;
p.on('binary', function(data) {
gotData = true;
assert.equal(originalMessage, getHexStringFromBuffer(data));
});
p.add(getBufferFromHexString(packet));
assert.ok(gotData);
},
'can parse a 200kb long unmasked binary message': function() {
var p = new Parser();
var length = 200 * 1024;
var message = new Buffer(length);
for (var i = 0; i < length; ++i) message[i] = i % 256;
var originalMessage = getHexStringFromBuffer(message);
var packet = '82 ' + getHybiLengthAsHexString(length, false) + ' ' + getHexStringFromBuffer(message);
var gotData = false;
p.on('binary', function(data) {
gotData = true;
assert.equal(originalMessage, getHexStringFromBuffer(data));
});
p.add(getBufferFromHexString(packet));
assert.ok(gotData);
},
};

View File

@@ -0,0 +1,262 @@
/**
* Test dependencies.
*/
var assert = require('assert');
var Parser = require('../lib/transports/websocket/hybi-16.js').Parser;
require('./hybi-common');
/**
* Tests.
*/
module.exports = {
'can parse unmasked text message': function() {
var p = new Parser();
var packet = '81 05 48 65 6c 6c 6f';
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal('Hello', data);
});
p.add(getBufferFromHexString(packet));
assert.ok(gotData);
},
'can parse close message': function() {
var p = new Parser();
var packet = '88 00';
var gotClose = false;
p.on('close', function(data) {
gotClose = true;
});
p.add(getBufferFromHexString(packet));
assert.ok(gotClose);
},
'can parse masked text message': function() {
var p = new Parser();
var packet = '81 93 34 83 a8 68 01 b9 92 52 4f a1 c6 09 59 e6 8a 52 16 e6 cb 00 5b a1 d5';
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal('5:::{"name":"echo"}', data);
});
p.add(getBufferFromHexString(packet));
assert.ok(gotData);
},
'can parse a masked text message longer than 125 bytes': function() {
var p = new Parser();
var message = 'A';
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
var packet = '81 FE ' + pack(4, message.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal(message, data);
});
p.add(getBufferFromHexString(packet));
assert.ok(gotData);
},
'can parse a really long masked text message': function() {
var p = new Parser();
var message = 'A';
for (var i = 0; i < 64*1024; ++i) message += (i % 5).toString();
var packet = '81 FF ' + pack(16, message.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal(message, data);
});
p.add(getBufferFromHexString(packet));
assert.ok(gotData);
},
'can parse a fragmented masked text message of 300 bytes': function() {
var p = new Parser();
var message = 'A';
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
var msgpiece1 = message.substr(0, 150);
var msgpiece2 = message.substr(150);
var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece1, '34 83 a8 68'));
var packet2 = '80 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece2, '34 83 a8 68'));
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal(message, data);
});
p.add(getBufferFromHexString(packet1));
p.add(getBufferFromHexString(packet2));
assert.ok(gotData);
},
'can parse a ping message': function() {
var p = new Parser();
var message = 'Hello';
var packet = '89 FE ' + pack(4, message.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
var gotPing = false;
p.on('ping', function(data) {
gotPing = true;
assert.equal(message, data);
});
p.add(getBufferFromHexString(packet));
assert.ok(gotPing);
},
'can parse a ping with no data': function() {
var p = new Parser();
var packet = '89 00';
var gotPing = false;
p.on('ping', function(data) {
gotPing = true;
});
p.add(getBufferFromHexString(packet));
assert.ok(gotPing);
},
'can parse a fragmented masked text message of 300 bytes with a ping in the middle': function() {
var p = new Parser();
var message = 'A';
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
var msgpiece1 = message.substr(0, 150);
var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece1, '34 83 a8 68'));
var pingMessage = 'Hello';
var pingPacket = '89 FE ' + pack(4, pingMessage.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(pingMessage, '34 83 a8 68'));
var msgpiece2 = message.substr(150);
var packet2 = '80 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece2, '34 83 a8 68'));
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal(message, data);
});
var gotPing = false;
p.on('ping', function(data) {
gotPing = true;
assert.equal(pingMessage, data);
});
p.add(getBufferFromHexString(packet1));
p.add(getBufferFromHexString(pingPacket));
p.add(getBufferFromHexString(packet2));
assert.ok(gotData);
assert.ok(gotPing);
},
'can parse a fragmented masked text message of 300 bytes with a ping in the middle, which is delievered over sevaral tcp packets': function() {
var p = new Parser();
var message = 'A';
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
var msgpiece1 = message.substr(0, 150);
var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece1, '34 83 a8 68'));
var pingMessage = 'Hello';
var pingPacket = '89 FE ' + pack(4, pingMessage.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(pingMessage, '34 83 a8 68'));
var msgpiece2 = message.substr(150);
var packet2 = '80 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece2, '34 83 a8 68'));
var gotData = false;
p.on('data', function(data) {
gotData = true;
assert.equal(message, data);
});
var gotPing = false;
p.on('ping', function(data) {
gotPing = true;
assert.equal(pingMessage, data);
});
var buffers = [];
buffers = buffers.concat(splitBuffer(getBufferFromHexString(packet1)));
buffers = buffers.concat(splitBuffer(getBufferFromHexString(pingPacket)));
buffers = buffers.concat(splitBuffer(getBufferFromHexString(packet2)));
for (var i = 0; i < buffers.length; ++i) {
p.add(buffers[i]);
}
assert.ok(gotData);
assert.ok(gotPing);
},
'can parse a 100 byte long masked binary message': function() {
var p = new Parser();
var length = 100;
var message = new Buffer(length);
for (var i = 0; i < length; ++i) message[i] = i % 256;
var originalMessage = getHexStringFromBuffer(message);
var packet = '82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
var gotData = false;
p.on('binary', function(data) {
gotData = true;
assert.equal(originalMessage, getHexStringFromBuffer(data));
});
p.add(getBufferFromHexString(packet));
assert.ok(gotData);
},
'can parse a 256 byte long masked binary message': function() {
var p = new Parser();
var length = 256;
var message = new Buffer(length);
for (var i = 0; i < length; ++i) message[i] = i % 256;
var originalMessage = getHexStringFromBuffer(message);
var packet = '82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
var gotData = false;
p.on('binary', function(data) {
gotData = true;
assert.equal(originalMessage, getHexStringFromBuffer(data));
});
p.add(getBufferFromHexString(packet));
assert.ok(gotData);
},
'can parse a 200kb long masked binary message': function() {
var p = new Parser();
var length = 200 * 1024;
var message = new Buffer(length);
for (var i = 0; i < length; ++i) message[i] = i % 256;
var originalMessage = getHexStringFromBuffer(message);
var packet = '82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
var gotData = false;
p.on('binary', function(data) {
gotData = true;
assert.equal(originalMessage, getHexStringFromBuffer(data));
});
p.add(getBufferFromHexString(packet));
assert.ok(gotData);
},
'can parse a 200kb long unmasked binary message': function() {
var p = new Parser();
var length = 200 * 1024;
var message = new Buffer(length);
for (var i = 0; i < length; ++i) message[i] = i % 256;
var originalMessage = getHexStringFromBuffer(message);
var packet = '82 ' + getHybiLengthAsHexString(length, false) + ' ' + getHexStringFromBuffer(message);
var gotData = false;
p.on('binary', function(data) {
gotData = true;
assert.equal(originalMessage, getHexStringFromBuffer(data));
});
p.add(getBufferFromHexString(packet));
assert.ok(gotData);
},
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff