rearranged repo structure

This commit is contained in:
logsol 2012-06-30 20:36:16 +02:00
parent 908a9dd43d
commit 5f5178d2e8
572 changed files with 27633 additions and 568 deletions

2
.tests/networking/node_modules/animation/.npmignore generated vendored Normal file
View file

@ -0,0 +1,2 @@
Cakefile
src/

162
.tests/networking/node_modules/animation/README.md generated vendored Normal file
View file

@ -0,0 +1,162 @@
# [animation](https://github.com/dodo/node-animation/)
Handles Animation Timing and Handling for you.
Uses requesAnimationFrame when running on browser side.
## Installation
```bash
$ npm install animation
```
## Usage
```javascript
// get a tick every 100ms
var animation = new Animation({frame:'100ms'});
animation.on('tick', function (dt) { … });
animation.start();
```
```javascript
// get next tick with delta time to last tick
var animation = new Animation({frame:'100ms'});
var animate = function (dt) {
// do your animation stuff
if (process.stdout.write(data)) {
animation.nextTick(animate);
} else {
var t = new Date().getTime()
process.stdout.once('drain', function () {
var now = new Date().getTime();
animate(now - t + dt);
});
}
};
animation.nextTick(animate); // no start required
```
```javascript
// doesnt really matter when its executed, but it should happen
// (use this in browser if you want to update your dom on requesAnimationFrame)
var animation = new Animation();
animation.start();
animation.push(function (dt) {
// happens (once) on the next few ticks,
// depending on how much tasks are allready pushed
});
```
[Δt](http://dodo.github.com/node-dynamictemplate/) adapters for [DOM](http://dodo.github.com/node-dt-dom/) and [jQuery](http://dodo.github.com/node-dt-dom/) depending on this module to do heavy DOM manipulation like insertion only on requesAnimationFrame.
[surrender-cube](https://github.com/dodo/node-surrender-cube/blob/master/src/index.coffee) uses this module to draw a rotating wireframe cube in terminal.
[ceilingled](https://github.com/c3d2/ceilingled) uses this to draw images fetched from superfeedr to draw either on SDL or on a LED wall.
## Animation
```javascript
animation = new Animation({
// defaults
timeoutexecution:'20ms', // allowed execution time per animation tick timeout
execution: '5ms', // allowed execution time per animation tick
timeout: null, // maximum time of a animation tick interval else runs continuously if null
toggle: false, // if true animation pauses and resumes itself when render queue gets empty or filled
frame: '16ms' // time per frame
});
```
Creates a new Animation controller.
### animation.start
```javascript
animation.start();
```
Starts animation.
### animation.stop
```javascript
animation.stop();
```
Stops animation.
### animation.pause
```javascript
animation.pause();
```
When autotoggle is enabled the Animation pauses itself if the render queue is empty.
### animation.resume
```javascript
animation.resume();
```
When autotoggle is enabled the Animation resumes itself when the render queue gets filled again after it was emtpy.
### animation.nextTick
```javascript
animation.nextTick(function (dt) { … });
```
Given callback gets called on next animation tick when running and not paused.
## Events
### 'start'
```javascript
animation.on('start', function () { … });
```
Emits `start` event every time the animation gets started.
### 'stop'
```javascript
animation.on('stop', function () { … });
```
Emits `stop` event every time the animation gets stopped.
### 'pause'
```javascript
animation.on('pause', function () { … });
```
Emits `pause` event every time the animation gets paused.
### 'resume'
```javascript
animation.on('resume', function () { … });
```
Emits `resume` event every time the animation gets resumed.
### 'tick'
```javascript
animation.on('tick', function (dt) { … });
```
Emits `tick` event every time the animation executes a animation tick.
`dt` is the time since last animation tick finished.
Use this to do your animation stuff.

View file

@ -0,0 +1,2 @@
module.exports = require('./lib/animation')

View file

@ -0,0 +1,125 @@
(function() {
var EventEmitter, cancelAnimationFrame, ms, now, requestAnimationFrame, _ref, _ref2;
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; };
EventEmitter = require('events').EventEmitter;
_ref = require('request-animation-frame'), requestAnimationFrame = _ref.requestAnimationFrame, cancelAnimationFrame = _ref.cancelAnimationFrame;
ms = require('ms');
now = (_ref2 = Date.now) != null ? _ref2 : function() {
return new Date().getTime();
};
this.Animation = (function() {
__extends(Animation, EventEmitter);
function Animation(opts) {
var _ref3, _ref4, _ref5;
if (opts == null) opts = {};
this.nextTick = __bind(this.nextTick, this);
this.timoutexecutiontime = ms((_ref3 = opts.timeoutexecution) != null ? _ref3 : '32ms');
this.executiontime = ms((_ref4 = opts.execution) != null ? _ref4 : '8ms');
this.timeouttime = opts.timeout;
if (this.timeouttime != null) this.timeouttime = ms(this.timeouttime);
this.autotoggle = (_ref5 = opts.toggle) != null ? _ref5 : false;
this.frametime = opts.frame;
if (this.frametime != null) this.frametime = ms(this.frametime);
this.queue = [];
this.running = false;
this.paused = false;
Animation.__super__.constructor.apply(this, arguments);
}
Animation.prototype.need_next_tick = function() {
return this.running && !this.paused && (this.queue.length || !this.autotoggle);
};
Animation.prototype.work_queue = function(started, dt, executiontime) {
var t, _base, _results;
t = now();
_results = [];
while (this.queue.length && t - started < executiontime) {
if (typeof (_base = this.queue.shift()) === "function") _base(dt);
_results.push(t = now());
}
return _results;
};
Animation.prototype.push = function(callback) {
this.queue.push(callback);
if (this.running && this.autotoggle) return this.resume();
};
Animation.prototype.nextTick = function(callback) {
var request, t, tick, timeout, _ref3;
_ref3 = [null, null], timeout = _ref3[0], request = _ref3[1];
t = now();
tick = function(success) {
var dt, executiontime, nextid, started;
if (this.need_next_tick()) nextid = this.nextTick();
started = now();
dt = started - t;
executiontime = success ? this.executiontime : this.timoutexecutiontime;
if (success) {
clearTimeout(timeout);
} else {
cancelAnimationFrame(request);
}
this.emit('tick', dt);
if (typeof callback === "function") callback(dt);
this.work_queue(started, dt, executiontime);
if (nextid == null) return;
if (!this.need_next_tick()) {
if (this.timeouttime != null) clearTimeout(nextid.timeout);
cancelAnimationFrame(nextid);
this.pause();
}
};
request = requestAnimationFrame(tick.bind(this, true), this.frametime);
if (this.timeouttime != null) {
timeout = setTimeout(tick.bind(this, false), this.timeouttime);
request.timeout = timeout;
}
return request;
};
Animation.prototype.start = function() {
if (this.running) return;
this.running = true;
this.emit('start');
if (!this.paused && this.autotoggle && !this.queue.length) {
return this.pause();
} else {
return this.nextTick();
}
};
Animation.prototype.stop = function() {
if (!this.running) return;
this.running = false;
return this.emit('stop');
};
Animation.prototype.pause = function() {
if (this.paused) return;
this.paused = true;
return this.emit('pause');
};
Animation.prototype.resume = function() {
if (!this.paused) return;
this.paused = false;
this.emit('resume');
if (this.running && (!this.autotoggle || this.queue.length === 1)) {
return this.nextTick();
}
};
return Animation;
})();
}).call(this);

View file

@ -0,0 +1 @@
node_modules

View file

@ -0,0 +1,8 @@
test:
./node_modules/.bin/mocha test/test.js
test-browser:
./node_modules/.bin/serve test/
.PHONY: test

View file

@ -0,0 +1,65 @@
# ms.js
Ever find yourself doing math in your head or writing `1000 * 60 * 60 …`?
Don't want to add obstrusive `Number` prototype extensions to your reusable
/ distributable modules and projects?
`ms` is a tiny utility that you can leverage when your application needs to
accept a number of miliseconds as a parameter.
If a number is supplied to `ms`, it returns it immediately (e.g:
If a string that contains the number is supplied, it returns it immediately as
a number (e.g: it returns `100` for `'100'`).
However, if you pass a string with a number and a valid unit, hte number of
equivalent ms is returned.
```js
ms('1d') // 86400000
ms('10h') // 36000000
ms('2h') // 7200000
ms('1m') // 60000
ms('5ms') // 5000
ms('100') // '100'
ms(100) // 100
```
## How to use
### Node
```js
require('ms')
```
### Browser
```html
<script src="ms.js"></script>
```
## Credits
(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,35 @@
/**
# ms.js
No more painful `setTimeout(fn, 60 * 4 * 3 * 2 * 1 * Infinity * NaN * '☃')`.
ms('2d') // 172800000
ms('1.5h') // 5400000
ms('1h') // 3600000
ms('1m') // 60000
ms('5s') // 5000
ms('500ms') // 500
ms('100') // '100'
ms(100) // 100
**/
(function (g) {
var r = /(\d*.?\d+)([mshd]+)/
, _ = {}
_.ms = 1;
_.s = 1000;
_.m = _.s * 60;
_.h = _.m * 60;
_.d = _.h * 24;
function ms (s) {
if (s == Number(s)) return Number(s);
r.exec(s.toLowerCase());
return RegExp.$1 * _[RegExp.$2];
}
g.top ? g.ms = ms : module.exports = ms;
})(this);

View file

@ -0,0 +1,25 @@
{
"name": "ms",
"version": "0.1.0",
"description": "Tiny ms conversion utility",
"main": "./ms",
"devDependencies": {
"mocha": "*",
"expect.js": "*",
"serve": "*"
},
"_id": "ms@0.1.0",
"dependencies": {},
"optionalDependencies": {},
"engines": {
"node": "*"
},
"_engineSupported": true,
"_npmVersion": "1.1.9",
"_nodeVersion": "v0.6.13",
"_defaultsLoaded": true,
"dist": {
"shasum": "dab9cb8baa222d6de25801476ef00410d1c87e3a"
},
"_from": "ms@>= 0.1.0"
}

View file

@ -0,0 +1,19 @@
<!doctype html>
<html>
<head>
<title>ms.js tests</title>
<link rel="stylesheet" href="/support/mocha.css" media="all">
<script src="/ms.js"></script>
<script src="/support/jquery.js"></script>
<script src="/support/expect.js"></script>
<script src="/support/mocha.js"></script>
<script>mocha.setup('bdd');</script>
<script src="/test.js"></script>
<script>window.onload = mocha.run;</script>
</head>
<body>
<div id="mocha"></div>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,61 @@
/**
* Dependencies.
*/
if ('undefined' != typeof require) {
expect = require('expect.js');
ms = require('../ms');
}
/**
* Test.
*/
describe('ms.js', function () {
it('should preserve ms', function () {
expect(100).to.be(100);
});
it('should convert number strings to number', function () {
expect(ms('1e+5')).to.be(1e+5);
});
it('should convert from m to ms', function () {
expect(ms('1m')).to.be(60000);
});
it('should convert from h to ms', function () {
expect(ms('1h')).to.be(3600000);
});
it('should convert d to ms', function () {
expect(ms('2d')).to.be(172800000);
});
it('should convert s to ms', function () {
expect(ms('1s')).to.be(1000);
});
it('should convert ms to ms', function () {
expect(ms('100ms')).to.be(100);
});
it('should work with decimals', function () {
expect(ms('1.5h')).to.be(5400000);
});
it('should return NaN if invalid', function () {
expect(isNaN(ms('☃'))).to.be(true);
});
it('should be case-insensitive', function () {
expect(ms('1.5H')).to.be(5400000);
});
it('should work with numbers starting with .', function () {
expect(ms('.5ms')).to.be(.5);
});
});

View file

@ -0,0 +1,2 @@
Cakefile
src/

View file

@ -0,0 +1,46 @@
(function() {
var max, now, _ref, _ref2;
now = (_ref = Date.now) != null ? _ref : function() {
return new Date().getTime();
};
max = Math.max;
_ref2 = (function() {
var cancel, isNative, last, request, vendor, _i, _len, _ref2;
last = 0;
request = typeof window !== "undefined" && window !== null ? window.requestAnimationFrame : void 0;
cancel = typeof window !== "undefined" && window !== null ? window.cancelAnimationFrame : void 0;
_ref2 = ["webkit", "moz", "o", "ms"];
for (_i = 0, _len = _ref2.length; _i < _len; _i++) {
vendor = _ref2[_i];
if (cancel == null) {
cancel = (typeof window !== "undefined" && window !== null ? window["" + vendor + "cancelAnimationFrame"] : void 0) || (typeof window !== "undefined" && window !== null ? window["" + vendor + "cancelRequestAnimationFrame"] : void 0);
}
if ((request != null ? request : request = typeof window !== "undefined" && window !== null ? window["" + vendor + "RequestAnimationFrame"] : void 0)) {
break;
}
}
isNative = request != null;
request = request != null ? request : function(callback, timeout) {
var cur, id, time;
if (timeout == null) timeout = 16;
cur = now();
time = max(0, timeout + last - cur);
id = setTimeout(function() {
return typeof callback === "function" ? callback(cur + time) : void 0;
}, time);
last = cur + time;
return id;
};
request.isNative = isNative;
isNative = cancel != null;
cancel = cancel != null ? cancel : function(id) {
return clearTimeout(id);
};
cancel.isNative = isNative;
return [request, cancel];
})(), this.requestAnimationFrame = _ref2[0], this.cancelAnimationFrame = _ref2[1];
}).call(this);

View file

@ -0,0 +1,44 @@
{
"name": "request-animation-frame",
"description": "requestAnimationFrame shim",
"version": "0.1.1",
"homepage": "https://github.com/dodo/requestAnimationFrame.js",
"author": {
"name": "dodo",
"url": "https://github.com/dodo"
},
"repository": {
"type": "git",
"url": "git://github.com/dodo/requestAnimationFrame.js.git"
},
"main": "shim.js",
"engines": {
"node": ">= 0.4.x"
},
"keywords": [
"request",
"animation",
"frame",
"shim",
"browser",
"polyfill"
],
"scripts": {
"prepublish": "cake build"
},
"devDependencies": {
"muffin": ">= 0.2.6",
"coffee-script": ">= 1.1.2"
},
"_id": "request-animation-frame@0.1.1",
"dependencies": {},
"optionalDependencies": {},
"_engineSupported": true,
"_npmVersion": "1.1.9",
"_nodeVersion": "v0.6.13",
"_defaultsLoaded": true,
"dist": {
"shasum": "26f667a9aa5e3f341068d2d644ce859903750c66"
},
"_from": "request-animation-frame@>= 0.1.0"
}

View file

@ -0,0 +1,2 @@
module.exports = require('./lib/shim')

47
.tests/networking/node_modules/animation/package.json generated vendored Normal file
View file

@ -0,0 +1,47 @@
{
"name": "animation",
"description": "animation timing & handling",
"version": "0.1.2",
"homepage": "https://github.com/dodo/node-animation",
"author": {
"name": "dodo",
"url": "https://github.com/dodo"
},
"repository": {
"type": "git",
"url": "git://github.com/dodo/node-animation.git"
},
"main": "animation.js",
"engines": {
"node": ">= 0.4.x"
},
"keywords": [
"request",
"animation",
"frame",
"interval",
"node",
"browser"
],
"scripts": {
"prepublish": "cake build"
},
"dependencies": {
"ms": ">= 0.1.0",
"request-animation-frame": ">= 0.1.0"
},
"devDependencies": {
"muffin": ">= 0.2.6",
"coffee-script": ">= 1.1.2"
},
"_id": "animation@0.1.2",
"optionalDependencies": {},
"_engineSupported": true,
"_npmVersion": "1.1.9",
"_nodeVersion": "v0.6.13",
"_defaultsLoaded": true,
"dist": {
"shasum": "3837c251dc74c747c3348066a63f81a002cf55ce"
},
"_from": "animation"
}

58
.tests/networking/node_modules/box2d/README generated vendored Normal file
View file

@ -0,0 +1,58 @@
@author Josh Adell <josh.adell@gmail.com>
A port of the box2djs library into a node module.
Here is the box2d "Hello World" program from http://www.box2d.org/ written for node:
--- BEGIN CODE EXAMPLE ---
var sys = require("sys"),
b2d = require("./box2dnode");
// Define world
var worldAABB = new b2d.b2AABB();
worldAABB.lowerBound.Set(-100.0, -100.0);
worldAABB.upperBound.Set(100.0, 100.0);
var gravity = new b2d.b2Vec2(0.0, -10.0);
var doSleep = true;
var world = new b2d.b2World(worldAABB, gravity, doSleep);
// Ground Box
var groundBodyDef = new b2d.b2BodyDef();
groundBodyDef.position.Set(0.0, -10.0);
var groundBody = world.CreateBody(groundBodyDef);
var groundShapeDef = new b2d.b2PolygonDef();
groundShapeDef.SetAsBox(50.0, 10.0);
groundBody.CreateShape(groundShapeDef);
// Dynamic Body
var bodyDef = new b2d.b2BodyDef();
bodyDef.position.Set(0.0, 4.0);
var body = world.CreateBody(bodyDef);
var shapeDef = new b2d.b2PolygonDef();
shapeDef.SetAsBox(1.0, 1.0);
shapeDef.density = 1.0;
shapeDef.friction = 0.3;
body.CreateShape(shapeDef);
body.SetMassFromShapes();
// Run Simulation!
var timeStep = 1.0 / 60.0;
var iterations = 10;
for (var i=0; i < 60; i++) {
world.Step(timeStep, iterations);
var position = body.GetPosition();
var angle = body.GetAngle();
sys.puts(i+": <"+position.x+", "+position.y+"> @"+angle);
}
--- END CODE EXAMPLE ---

11132
.tests/networking/node_modules/box2d/box2dnode.js generated vendored Normal file

File diff suppressed because it is too large Load diff

24
.tests/networking/node_modules/box2d/package.json generated vendored Normal file
View file

@ -0,0 +1,24 @@
{
"name": "box2d",
"version": "1.0.0",
"description": "2D physics engine",
"homepage": "http://github.com/jadell/box2dnode",
"author": {
"name": "Josh Adell",
"email": "josh.adell@gmail.com",
"url": "http://everymansoftware.blogspot.com/"
},
"main": "./box2dnode",
"engines": [
"node"
],
"_id": "box2d@1.0.0",
"dependencies": {},
"devDependencies": {},
"optionalDependencies": {},
"_engineSupported": true,
"_npmVersion": "1.1.4",
"_nodeVersion": "v0.6.12",
"_defaultsLoaded": true,
"_from": "box2d"
}

3
.tests/networking/node_modules/socket.io/.npmignore generated vendored Normal file
View file

@ -0,0 +1,3 @@
support
test
examples

6
.tests/networking/node_modules/socket.io/.travis.yml generated vendored Normal file
View file

@ -0,0 +1,6 @@
language: node_js
node_js:
- 0.6
notifications:
irc: "irc.freenode.org#socket.io"

260
.tests/networking/node_modules/socket.io/History.md generated vendored Normal file
View file

@ -0,0 +1,260 @@
0.9.6 / 2012-04-17
==================
* Fixed XSS in jsonp-polling.
0.9.5 / 2012-04-05
==================
* Added test for polling and socket close.
* Ensure close upon request close.
* Fix disconnection reason being lost for polling transports.
* Ensure that polling transports work with Connection: close.
* Log disconnection reason.
0.9.4 / 2012-04-01
==================
* Disconnecting from namespace improvement (#795) [DanielBaulig]
* Bumped client with polling reconnection loop (#438)
0.9.3 / 2012-03-28
==================
* Fix "Syntax error" on FF Web Console with XHR Polling [mikito]
0.9.2 / 2012-03-13
==================
* More sensible close `timeout default` (fixes disconnect issue)
0.9.1-1 / 2012-03-02
====================
* Bumped client with NPM dependency fix.
0.9.1 / 2012-03-02
==================
* Changed heartbeat timeout and interval defaults (60 and 25 seconds)
* Make tests work both on 0.4 and 0.6
* Updated client (improvements + bug fixes).
0.9.0 / 2012-02-26
==================
* Make it possible to use a regexp to match the socket.io resource URL.
We need this because we have to prefix the socket.io URL with a variable ID.
* Supplemental fix to gavinuhma/authfix, it looks like the same Access-Control-Origin logic is needed in the http and xhr-polling transports
* Updated express dep for windows compatibility.
* Combine two substr calls into one in decodePayload to improve performance
* Minor documentation fix
* Minor. Conform to style of other files.
* Switching setting to 'match origin protocol'
* Revert "Fixes leaking Redis subscriptions for #663. The local flag was not getting passed through onClientDisconnect()."
* Revert "Handle leaked dispatch:[id] subscription."
* Merge pull request #667 from dshaw/patch/redis-disconnect
* Handle leaked dispatch:[id] subscription.
* Fixes leaking Redis subscriptions for #663. The local flag was not getting passed through onClientDisconnect().
* Prevent memory leaking on uncompleted requests & add max post size limitation
* Fix for testcase
* Set Access-Control-Allow-Credentials true, regardless of cookie
* Remove assertvarnish from package as it breaks on 0.6
* Correct irc channel
* Added proper return after reserved field error
* Fixes manager.js failure to close connection after transport error has happened
* Added implicit port 80 for origin checks. fixes #638
* Fixed bug #432 in 0.8.7
* Set Access-Control-Allow-Origin header to origin to enable withCredentials
* Adding configuration variable matchOriginProtocol
* Fixes location mismatch error in Safari.
* Use tty to detect if we should add colors or not by default.
* Updated the package location.
0.8.7 / 2011-11-05
==================
* Fixed memory leaks in closed clients.
* Fixed memory leaks in namespaces.
* Fixed websocket handling for malformed requests from proxies. [einaros]
* Node 0.6 compatibility. [einaros] [3rd-Eden]
* Adapted tests and examples.
0.8.6 / 2011-10-27
==================
* Added JSON decoding on jsonp-polling transport.
* Fixed README example.
* Major speed optimizations [3rd-Eden] [einaros] [visionmedia]
* Added decode/encode benchmarks [visionmedia]
* Added support for black-listing client sent events.
* Fixed logging options, closes #540 [3rd-Eden]
* Added vary header for gzip [3rd-Eden]
* Properly cleaned up async websocket / flashsocket tests, after patching node-websocket-client
* Patched to properly shut down when a finishClose call is made during connection establishment
* Added support for socket.io version on url and far-future Expires [3rd-Eden] [getify]
* Began IE10 compatibility [einaros] [tbranyen]
* Misc WebSocket fixes [einaros]
* Added UTF8 to respone headers for htmlfile [3rd-Eden]
0.8.5 / 2011-10-07
==================
* Added websocket draft HyBi-16 support. [einaros]
* Fixed websocket continuation bugs. [einaros]
* Fixed flashsocket transport name.
* Fixed websocket tests.
* Ensured `parser#decodePayload` doesn't choke.
* Added http referrer verification to manager verifyOrigin.
* Added access control for cross domain xhr handshakes [3rd-Eden]
* Added support for automatic generation of socket.io files [3rd-Eden]
* Added websocket binary support [einaros]
* Added gzip support for socket.io.js [3rd-Eden]
* Expose socket.transport [3rd-Eden]
* Updated client.
0.8.4 / 2011-09-06
==================
* Client build
0.8.3 / 2011-09-03
==================
* Fixed `\n` parsing for non-JSON packets (fixes #479).
* Fixed parsing of certain unicode characters (fixes #451).
* Fixed transport message packet logging.
* Fixed emission of `error` event resulting in an uncaught exception if unhandled (fixes #476).
* Fixed; allow for falsy values as the configuration value of `log level` (fixes #491).
* Fixed repository URI in `package.json`. Fixes #504.
* Added text/plain content-type to handshake responses [einaros]
* Improved single byte writes [einaros]
* Updated socket.io-flashsocket default port from 843 to 10843 [3rd-Eden]
* Updated client.
0.8.2 / 2011-08-29
==================
* Updated client.
0.8.1 / 2011-08-29
==================
* Fixed utf8 bug in send framing in websocket [einaros]
* Fixed typo in docs [Znarkus]
* Fixed bug in send framing for over 64kB of data in websocket [einaros]
* Corrected ping handling in websocket transport [einaros]
0.8.0 / 2011-08-28
==================
* Updated to work with two-level websocket versioning. [einaros]
* Added hybi07 support. [einaros]
* Added hybi10 support. [einaros]
* Added http referrer verification to manager.js verifyOrigin. [einaors]
0.7.11 / 2011-08-27
===================
* Updated socket.io-client.
0.7.10 / 2011-08-27
===================
* Updated socket.io-client.
0.7.9 / 2011-08-12
==================
* Updated socket.io-client.
* Make sure we only do garbage collection when the server we receive is actually run.
0.7.8 / 2011-08-08
==================
* Changed; make sure sio#listen passes options to both HTTP server and socket.io manager.
* Added docs for sio#listen.
* Added options parameter support for Manager constructor.
* Added memory leaks tests and test-leaks Makefile task.
* Removed auto npm-linking from make test.
* Make sure that you can disable heartbeats. [3rd-Eden]
* Fixed rooms memory leak [3rd-Eden]
* Send response once we got all POST data, not immediately [Pita]
* Fixed onLeave behavior with missing clientsk [3rd-Eden]
* Prevent duplicate references in rooms.
* Added alias for `to` to `in` and `in` to `to`.
* Fixed roomClients definition.
* Removed dependency on redis for installation without npm [3rd-Eden]
* Expose path and querystring in handshakeData [3rd-Eden]
0.7.7 / 2011-07-12
==================
* Fixed double dispatch handling with emit to closed clients.
* Added test for emitting to closed clients to prevent regression.
* Fixed race condition in redis test.
* Changed Transport#end instrumentation.
* Leveraged $emit instead of emit internally.
* Made tests faster.
* Fixed double disconnect events.
* Fixed disconnect logic
* Simplified remote events handling in Socket.
* Increased testcase timeout.
* Fixed unknown room emitting (GH-291). [3rd-Eden]
* Fixed `address` in handshakeData. [3rd-Eden]
* Removed transports definition in chat example.
* Fixed room cleanup
* Fixed; make sure the client is cleaned up after booting.
* Make sure to mark the client as non-open if the connection is closed.
* Removed unneeded `buffer` declarations.
* Fixed; make sure to clear socket handlers and subscriptions upon transport close.
0.7.6 / 2011-06-30
==================
* Fixed general dispatching when a client has closed.
0.7.5 / 2011-06-30
==================
* Fixed dispatching to clients that are disconnected.
0.7.4 / 2011-06-30
==================
* Fixed; only clear handlers if they were set. [level09]
0.7.3 / 2011-06-30
==================
* Exposed handshake data to clients.
* Refactored dispatcher interface.
* Changed; Moved id generation method into the manager.
* Added sub-namespace authorization. [3rd-Eden]
* Changed; normalized SocketNamespace local eventing [dvv]
* Changed; Use packet.reason or default to 'packet' [3rd-Eden]
* Changed console.error to console.log.
* Fixed; bind both servers at the same time do that the test never times out.
* Added 304 support.
* Removed `Transport#name` for abstract interface.
* Changed; lazily require http and https module only when needed. [3rd-Eden]
0.7.2 / 2011-06-22
==================
* Make sure to write a packet (of type `noop`) when closing a poll.
This solves a problem with cross-domain requests being flagged as aborted and
reconnection being triggered.
* Added `noop` message type.
0.7.1 / 2011-06-21
==================
* Fixed cross-domain XHR.
* Added CORS test to xhr-polling suite.
0.7.0 / 2010-06-21
==================
* http://socket.io/announcement.html

31
.tests/networking/node_modules/socket.io/Makefile generated vendored Normal file
View file

@ -0,0 +1,31 @@
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:
@$(MAKE) NODE_PATH=lib TESTS="$(ALL_TESTS)" run-tests
test-cov:
@TESTFLAGS=--cov $(MAKE) 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

345
.tests/networking/node_modules/socket.io/Readme.md generated vendored Normal file
View file

@ -0,0 +1,345 @@
# Socket.IO
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.
## How to Install
```bash
npm install socket.io
```
## How to use
First, require `socket.io`:
```js
var io = require('socket.io');
```
Next, attach it to a HTTP/HTTPS server. If you're using the fantastic `express`
web framework:
```js
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);
});
});
```
Finally, load it from the client side code:
```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
// 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');
});
});
```
### Storing data associated to a client
Sometimes it's necessary to store data associated with a client that's
necessary for the duration of the session.
#### Server side
```js
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);
});
});
});
```
#### Client side
```html
<script>
var socket = io.connect('http://localhost');
socket.on('connect', function () {
socket.emit('set nickname', confirm('What is your nickname?'));
socket.on('ready', function () {
console.log('Connected !');
socket.emit('msg', confirm('What is your message?'));
});
});
</script>
```
### Restricting yourself to a namespace
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;
}

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

@ -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);
});
});

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

@ -0,0 +1,11 @@
{
"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

@ -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

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

8
.tests/networking/node_modules/socket.io/index.js generated vendored Normal file
View file

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

167
.tests/networking/node_modules/socket.io/lib/client.js generated vendored Normal file
View file

@ -0,0 +1,167 @@
/**
* Module dependencies.
*/
var parser = require('socket.io-client').parser
, EventEmitter = require('events').EventEmitter
/**
* Client constructor.
*
* @api public
*/
function Client (id, server) {
this.id = id;
this.acks = {};
this.store = server.store;
var self = this;
store.subscribe(id, function (packet) {
});
store.subscribe(id + '.disconect', function () {
self.onDisconnect();
});
}
/**
* Inherits from EventEmitter.
*/
Client.prototype.__proto__ = EventEmitter.prototype;
/**
* Save reference to original `emit`.
*
* @api private
*/
Client.prototype._emit = Client.prototype.emit;
/**
* Broadcast flag.
*
* @api public
*/
Client.prototype.__defineGetter__('broadcast', function () {
this.flags.broadcast = true;
});
/**
* JSON flag (deprecated)
*
* @api public
*/
Client.prototype.__defineGetter__('json', function () {
this.flags.broadcast = true;
});
/**
* Joins a group.
*
* @param {String} group
* @return {Client} for chaining
* @api public
*/
Client.prototype.join = function (group, fn) {
if (!~this.subscriptions.indexOf(group)) {
var self = this;
this.subscriptions.push(group);
this.store.addToGroup(group, this.sid, function (ev, args) {
self.onGroupEvent(ev, args);
}, fn);
} else {
fn && fn();
}
return this;
};
/**
* Leaves a group.
*
* @return {Client} for chaining
* @api public
*/
Client.prototype.leave = function (group) {
var index = this.subscriptions.indexOf(group);
if (~index) {
this.subscriptions.splice(index, 1);
}
return this;
};
Client.prototype.disconnect = function () {
if (this.socket) {
this.socket.disconnect();
} else {
this.publish('disconnect');
}
}
/**
* Called upon disconnect.
*
* @api private
*/
Client.prototype.onDisconnect = function () {
for (var i = 0, l = this.subscriptions; i < l; i++) {
this.store.removeFromGroup(id, group, fn);
}
};
/**
* Registers ACK.
*/
Client.prototype.ack = function (fn, callback) {
this.subscribe('ack');
};
/**
* Emits an event.
*/
Client.prototype.emit = function () {
var args = toArray(arguments), fn;
if ('function' == typeof args[args.length - 1]) {
fn = args.pop();
}
var data = args.shift();
if (args.length) {
data += '\n' + JSON.stringify(args);
}
if (fn) {
this.ack(fn, function (id) {
self.sendPacket('event', data, id);
});
} else {
this.sendPacket('event', data);
}
return this;
};
/**
* Sends a packet.
*/
Client.prototype.sendPacket = function (type, data, id) {
var data = parser.encode({ type: type, data: data, id: id });
if (this.server.sockets[id]) {
this.server.sockets[id].write(data);
}
};

97
.tests/networking/node_modules/socket.io/lib/logger.js generated vendored 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)));
};
});

984
.tests/networking/node_modules/socket.io/lib/manager.js generated vendored Normal file
View file

@ -0,0 +1,984 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var fs = require('fs')
, url = require('url')
, tty = require('tty')
, util = require('./util')
, store = require('./store')
, client = require('socket.io-client')
, transports = require('./transports')
, Logger = require('./logger')
, Socket = require('./socket')
, MemoryStore = require('./stores/memory')
, SocketNamespace = require('./namespace')
, Static = require('./static')
, EventEmitter = process.EventEmitter;
/**
* Export the constructor.
*/
exports = module.exports = Manager;
/**
* Default transports.
*/
var defaultTransports = exports.defaultTransports = [
'websocket'
, 'htmlfile'
, 'xhr-polling'
, 'jsonp-polling'
];
/**
* Inherited defaults.
*/
var parent = module.parent.exports
, protocol = parent.protocol
, jsonpolling_re = /^\d+$/;
/**
* Manager constructor.
*
* @param {HTTPServer} server
* @param {Object} options, optional
* @api public
*/
function Manager (server, options) {
this.server = server;
this.namespaces = {};
this.sockets = this.of('');
this.settings = {
origins: '*:*'
, log: true
, store: new MemoryStore
, logger: new Logger
, static: new Static(this)
, heartbeats: true
, resource: '/socket.io'
, transports: defaultTransports
, authorization: false
, blacklist: ['disconnect']
, 'log level': 3
, 'log colors': tty.isatty(process.stdout.fd)
, 'close timeout': 60
, 'heartbeat interval': 25
, 'heartbeat timeout': 60
, 'polling duration': 20
, 'flash policy server': true
, 'flash policy port': 10843
, 'destroy upgrade': true
, 'destroy buffer size': 10E7
, 'browser client': true
, 'browser client cache': true
, 'browser client minification': false
, 'browser client etag': false
, 'browser client expires': 315360000
, 'browser client gzip': false
, 'browser client handler': false
, 'client store expiration': 15
, 'match origin protocol': false
};
for (var i in options) {
this.settings[i] = options[i];
}
var self = this;
// default error handler
server.on('error', function(err) {
self.log.warn('error raised: ' + err);
});
this.initStore();
this.on('set:store', function() {
self.initStore();
});
// reset listeners
this.oldListeners = server.listeners('request');
server.removeAllListeners('request');
server.on('request', function (req, res) {
self.handleRequest(req, res);
});
server.on('upgrade', function (req, socket, head) {
self.handleUpgrade(req, socket, head);
});
server.on('close', function () {
clearInterval(self.gc);
});
server.once('listening', function () {
self.gc = setInterval(self.garbageCollection.bind(self), 10000);
});
for (var i in transports) {
if (transports[i].init) {
transports[i].init(this);
}
}
// forward-compatibility with 1.0
var self = this;
this.sockets.on('connection', function (conn) {
self.emit('connection', conn);
});
this.log.info('socket.io started');
};
Manager.prototype.__proto__ = EventEmitter.prototype
/**
* Store accessor shortcut.
*
* @api public
*/
Manager.prototype.__defineGetter__('store', function () {
var store = this.get('store');
store.manager = this;
return store;
});
/**
* Logger accessor.
*
* @api public
*/
Manager.prototype.__defineGetter__('log', function () {
var logger = this.get('logger');
logger.level = this.get('log level') || -1;
logger.colors = this.get('log colors');
logger.enabled = this.enabled('log');
return logger;
});
/**
* Static accessor.
*
* @api public
*/
Manager.prototype.__defineGetter__('static', function () {
return this.get('static');
});
/**
* Get settings.
*
* @api public
*/
Manager.prototype.get = function (key) {
return this.settings[key];
};
/**
* Set settings
*
* @api public
*/
Manager.prototype.set = function (key, value) {
if (arguments.length == 1) return this.get(key);
this.settings[key] = value;
this.emit('set:' + key, this.settings[key], key);
return this;
};
/**
* Enable a setting
*
* @api public
*/
Manager.prototype.enable = function (key) {
this.settings[key] = true;
this.emit('set:' + key, this.settings[key], key);
return this;
};
/**
* Disable a setting
*
* @api public
*/
Manager.prototype.disable = function (key) {
this.settings[key] = false;
this.emit('set:' + key, this.settings[key], key);
return this;
};
/**
* Checks if a setting is enabled
*
* @api public
*/
Manager.prototype.enabled = function (key) {
return !!this.settings[key];
};
/**
* Checks if a setting is disabled
*
* @api public
*/
Manager.prototype.disabled = function (key) {
return !this.settings[key];
};
/**
* Configure callbacks.
*
* @api public
*/
Manager.prototype.configure = function (env, fn) {
if ('function' == typeof env) {
env.call(this);
} else if (env == (process.env.NODE_ENV || 'development')) {
fn.call(this);
}
return this;
};
/**
* Initializes everything related to the message dispatcher.
*
* @api private
*/
Manager.prototype.initStore = function () {
this.handshaken = {};
this.connected = {};
this.open = {};
this.closed = {};
this.rooms = {};
this.roomClients = {};
var self = this;
this.store.subscribe('handshake', function (id, data) {
self.onHandshake(id, data);
});
this.store.subscribe('connect', function (id) {
self.onConnect(id);
});
this.store.subscribe('open', function (id) {
self.onOpen(id);
});
this.store.subscribe('join', function (id, room) {
self.onJoin(id, room);
});
this.store.subscribe('leave', function (id, room) {
self.onLeave(id, room);
});
this.store.subscribe('close', function (id) {
self.onClose(id);
});
this.store.subscribe('dispatch', function (room, packet, volatile, exceptions) {
self.onDispatch(room, packet, volatile, exceptions);
});
this.store.subscribe('disconnect', function (id) {
self.onDisconnect(id);
});
};
/**
* Called when a client handshakes.
*
* @param text
*/
Manager.prototype.onHandshake = function (id, data) {
this.handshaken[id] = data;
};
/**
* Called when a client connects (ie: transport first opens)
*
* @api private
*/
Manager.prototype.onConnect = function (id) {
this.connected[id] = true;
};
/**
* Called when a client opens a request in a different node.
*
* @api private
*/
Manager.prototype.onOpen = function (id) {
this.open[id] = true;
// if we were buffering messages for the client, clear them
if (this.closed[id]) {
var self = this;
this.store.unsubscribe('dispatch:' + id, function () {
delete self.closed[id];
});
}
// clear the current transport
if (this.transports[id]) {
this.transports[id].discard();
this.transports[id] = null;
}
};
/**
* Called when a message is sent to a namespace and/or room.
*
* @api private
*/
Manager.prototype.onDispatch = function (room, packet, volatile, exceptions) {
if (this.rooms[room]) {
for (var i = 0, l = this.rooms[room].length; i < l; i++) {
var id = this.rooms[room][i];
if (!~exceptions.indexOf(id)) {
if (this.transports[id] && this.transports[id].open) {
this.transports[id].onDispatch(packet, volatile);
} else if (!volatile) {
this.onClientDispatch(id, packet);
}
}
}
}
};
/**
* Called when a client joins a nsp / room.
*
* @api private
*/
Manager.prototype.onJoin = function (id, name) {
if (!this.roomClients[id]) {
this.roomClients[id] = {};
}
if (!this.rooms[name]) {
this.rooms[name] = [];
}
if (!~this.rooms[name].indexOf(id)) {
this.rooms[name].push(id);
this.roomClients[id][name] = true;
}
};
/**
* Called when a client leaves a nsp / room.
*
* @param private
*/
Manager.prototype.onLeave = function (id, room) {
if (this.rooms[room]) {
var index = this.rooms[room].indexOf(id);
if (index >= 0) {
this.rooms[room].splice(index, 1);
}
if (!this.rooms[room].length) {
delete this.rooms[room];
}
delete this.roomClients[id][room];
}
};
/**
* Called when a client closes a request in different node.
*
* @api private
*/
Manager.prototype.onClose = function (id) {
if (this.open[id]) {
delete this.open[id];
}
this.closed[id] = [];
var self = this;
this.store.subscribe('dispatch:' + id, function (packet, volatile) {
if (!volatile) {
self.onClientDispatch(id, packet);
}
});
};
/**
* Dispatches a message for a closed client.
*
* @api private
*/
Manager.prototype.onClientDispatch = function (id, packet) {
if (this.closed[id]) {
this.closed[id].push(packet);
}
};
/**
* Receives a message for a client.
*
* @api private
*/
Manager.prototype.onClientMessage = function (id, packet) {
if (this.namespaces[packet.endpoint]) {
this.namespaces[packet.endpoint].handlePacket(id, packet);
}
};
/**
* Fired when a client disconnects (not triggered).
*
* @api private
*/
Manager.prototype.onClientDisconnect = function (id, reason) {
for (var name in this.namespaces) {
this.namespaces[name].handleDisconnect(id, reason, typeof this.roomClients[id] !== 'undefined' &&
typeof this.roomClients[id][name] !== 'undefined');
}
this.onDisconnect(id);
};
/**
* Called when a client disconnects.
*
* @param text
*/
Manager.prototype.onDisconnect = function (id, local) {
delete this.handshaken[id];
if (this.open[id]) {
delete this.open[id];
}
if (this.connected[id]) {
delete this.connected[id];
}
if (this.transports[id]) {
this.transports[id].discard();
delete this.transports[id];
}
if (this.closed[id]) {
delete this.closed[id];
}
if (this.roomClients[id]) {
for (var room in this.roomClients[id]) {
this.onLeave(id, room);
}
delete this.roomClients[id]
}
this.store.destroyClient(id, this.get('client store expiration'));
this.store.unsubscribe('dispatch:' + id);
if (local) {
this.store.unsubscribe('message:' + id);
this.store.unsubscribe('disconnect:' + id);
}
};
/**
* Handles an HTTP request.
*
* @api private
*/
Manager.prototype.handleRequest = function (req, res) {
var data = this.checkRequest(req);
if (!data) {
for (var i = 0, l = this.oldListeners.length; i < l; i++) {
this.oldListeners[i].call(this.server, req, res);
}
return;
}
if (data.static || !data.transport && !data.protocol) {
if (data.static && this.enabled('browser client')) {
this.static.write(data.path, req, res);
} else {
res.writeHead(200);
res.end('Welcome to socket.io.');
this.log.info('unhandled socket.io url');
}
return;
}
if (data.protocol != protocol) {
res.writeHead(500);
res.end('Protocol version not supported.');
this.log.info('client protocol version unsupported');
} else {
if (data.id) {
this.handleHTTPRequest(data, req, res);
} else {
this.handleHandshake(data, req, res);
}
}
};
/**
* Handles an HTTP Upgrade.
*
* @api private
*/
Manager.prototype.handleUpgrade = function (req, socket, head) {
var data = this.checkRequest(req)
, self = this;
if (!data) {
if (this.enabled('destroy upgrade')) {
socket.end();
this.log.debug('destroying non-socket.io upgrade');
}
return;
}
req.head = head;
this.handleClient(data, req);
};
/**
* Handles a normal handshaken HTTP request (eg: long-polling)
*
* @api private
*/
Manager.prototype.handleHTTPRequest = function (data, req, res) {
req.res = res;
this.handleClient(data, req);
};
/**
* Intantiantes a new client.
*
* @api private
*/
Manager.prototype.handleClient = function (data, req) {
var socket = req.socket
, store = this.store
, self = this;
if (undefined != data.query.disconnect) {
if (this.transports[data.id] && this.transports[data.id].open) {
this.transports[data.id].onForcedDisconnect();
} else {
this.store.publish('disconnect-force:' + data.id);
}
return;
}
if (!~this.get('transports').indexOf(data.transport)) {
this.log.warn('unknown transport: "' + data.transport + '"');
req.connection.end();
return;
}
var transport = new transports[data.transport](this, data, req)
, handshaken = this.handshaken[data.id];
if (transport.disconnected) {
// failed during transport setup
req.connection.end();
return;
}
if (handshaken) {
if (transport.open) {
if (this.closed[data.id] && this.closed[data.id].length) {
transport.payload(this.closed[data.id]);
this.closed[data.id] = [];
}
this.onOpen(data.id);
this.store.publish('open', data.id);
this.transports[data.id] = transport;
}
if (!this.connected[data.id]) {
this.onConnect(data.id);
this.store.publish('connect', data.id);
// flag as used
delete handshaken.issued;
this.onHandshake(data.id, handshaken);
this.store.publish('handshake', data.id, handshaken);
// initialize the socket for all namespaces
for (var i in this.namespaces) {
var socket = this.namespaces[i].socket(data.id, true);
// echo back connect packet and fire connection event
if (i === '') {
this.namespaces[i].handlePacket(data.id, { type: 'connect' });
}
}
this.store.subscribe('message:' + data.id, function (packet) {
self.onClientMessage(data.id, packet);
});
this.store.subscribe('disconnect:' + data.id, function (reason) {
self.onClientDisconnect(data.id, reason);
});
}
} else {
if (transport.open) {
transport.error('client not handshaken', 'reconnect');
}
transport.discard();
}
};
/**
* Generates a session id.
*
* @api private
*/
Manager.prototype.generateId = function () {
return Math.abs(Math.random() * Math.random() * Date.now() | 0).toString()
+ Math.abs(Math.random() * Math.random() * Date.now() | 0).toString();
};
/**
* Handles a handshake request.
*
* @api private
*/
Manager.prototype.handleHandshake = function (data, req, res) {
var self = this
, origin = req.headers.origin
, headers = {
'Content-Type': 'text/plain'
};
function writeErr (status, message) {
if (data.query.jsonp && jsonpolling_re.test(data.query.jsonp)) {
res.writeHead(200, { 'Content-Type': 'application/javascript' });
res.end('io.j[' + data.query.jsonp + '](new Error("' + message + '"));');
} else {
res.writeHead(status, headers);
res.end(message);
}
};
function error (err) {
writeErr(500, 'handshake error');
self.log.warn('handshake error ' + err);
};
if (!this.verifyOrigin(req)) {
writeErr(403, 'handshake bad origin');
return;
}
var handshakeData = this.handshakeData(data);
if (origin) {
// https://developer.mozilla.org/En/HTTP_Access_Control
headers['Access-Control-Allow-Origin'] = origin;
headers['Access-Control-Allow-Credentials'] = 'true';
}
this.authorize(handshakeData, function (err, authorized, newData) {
if (err) return error(err);
if (authorized) {
var id = self.generateId()
, hs = [
id
, self.enabled('heartbeats') ? self.get('heartbeat timeout') || '' : ''
, self.get('close timeout') || ''
, self.transports(data).join(',')
].join(':');
if (data.query.jsonp && jsonpolling_re.test(data.query.jsonp)) {
hs = 'io.j[' + data.query.jsonp + '](' + JSON.stringify(hs) + ');';
res.writeHead(200, { 'Content-Type': 'application/javascript' });
} else {
res.writeHead(200, headers);
}
res.end(hs);
self.onHandshake(id, newData || handshakeData);
self.store.publish('handshake', id, newData || handshakeData);
self.log.info('handshake authorized', id);
} else {
writeErr(403, 'handshake unauthorized');
self.log.info('handshake unauthorized');
}
})
};
/**
* Gets normalized handshake data
*
* @api private
*/
Manager.prototype.handshakeData = function (data) {
var connection = data.request.connection
, connectionAddress
, date = new Date;
if (connection.remoteAddress) {
connectionAddress = {
address: connection.remoteAddress
, port: connection.remotePort
};
} else if (connection.socket && connection.socket.remoteAddress) {
connectionAddress = {
address: connection.socket.remoteAddress
, port: connection.socket.remotePort
};
}
return {
headers: data.headers
, address: connectionAddress
, time: date.toString()
, query: data.query
, url: data.request.url
, xdomain: !!data.request.headers.origin
, secure: data.request.connection.secure
, issued: +date
};
};
/**
* Verifies the origin of a request.
*
* @api private
*/
Manager.prototype.verifyOrigin = function (request) {
var origin = request.headers.origin || request.headers.referer
, origins = this.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 handshake, yet required by config');
}
return false;
};
/**
* Handles an incoming packet.
*
* @api private
*/
Manager.prototype.handlePacket = function (sessid, packet) {
this.of(packet.endpoint || '').handlePacket(sessid, packet);
};
/**
* Performs authentication.
*
* @param Object client request data
* @api private
*/
Manager.prototype.authorize = function (data, fn) {
if (this.get('authorization')) {
var self = this;
this.get('authorization').call(this, data, function (err, authorized) {
self.log.debug('client ' + authorized ? 'authorized' : 'unauthorized');
fn(err, authorized);
});
} else {
this.log.debug('client authorized');
fn(null, true);
}
return this;
};
/**
* Retrieves the transports adviced to the user.
*
* @api private
*/
Manager.prototype.transports = function (data) {
var transp = this.get('transports')
, ret = [];
for (var i = 0, l = transp.length; i < l; i++) {
var transport = transp[i];
if (transport) {
if (!transport.checkClient || transport.checkClient(data)) {
ret.push(transport);
}
}
}
return ret;
};
/**
* Checks whether a request is a socket.io one.
*
* @return {Object} a client request data object or `false`
* @api private
*/
var regexp = /^\/([^\/]+)\/?([^\/]+)?\/?([^\/]+)?\/?$/
Manager.prototype.checkRequest = function (req) {
var resource = this.get('resource');
var match;
if (typeof resource === 'string') {
match = req.url.substr(0, resource.length);
if (match !== resource) match = null;
} else {
match = resource.exec(req.url);
if (match) match = match[0];
}
if (match) {
var uri = url.parse(req.url.substr(match.length), true)
, path = uri.pathname || ''
, pieces = path.match(regexp);
// client request data
var data = {
query: uri.query || {}
, headers: req.headers
, request: req
, path: path
};
if (pieces) {
data.protocol = Number(pieces[1]);
data.transport = pieces[2];
data.id = pieces[3];
data.static = !!this.static.has(path);
};
return data;
}
return false;
};
/**
* Declares a socket namespace
*
* @api public
*/
Manager.prototype.of = function (nsp) {
if (this.namespaces[nsp]) {
return this.namespaces[nsp];
}
return this.namespaces[nsp] = new SocketNamespace(this, nsp);
};
/**
* Perform garbage collection on long living objects and properties that cannot
* be removed automatically.
*
* @api private
*/
Manager.prototype.garbageCollection = function () {
// clean up unused handshakes
var ids = Object.keys(this.handshaken)
, i = ids.length
, now = Date.now()
, handshake;
while (i--) {
handshake = this.handshaken[ids[i]];
if ('issued' in handshake && (now - handshake.issued) >= 3E4) {
this.onDisconnect(ids[i]);
}
}
};

View file

@ -0,0 +1,355 @@
/**
* Module dependencies.
*/
var Socket = require('./socket')
, EventEmitter = process.EventEmitter
, parser = require('./parser')
, util = require('./util');
/**
* Exports the constructor.
*/
exports = module.exports = SocketNamespace;
/**
* 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.
*
* @api private
*/
SocketNamespace.prototype.$emit = EventEmitter.prototype.emit;
/**
* Retrieves all clients as Socket instances as an array.
*
* @api public
*/
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);
};
/**
* Access logger interface.
*
* @api public
*/
SocketNamespace.prototype.__defineGetter__('log', function () {
return this.manager.log;
});
/**
* Access store.
*
* @api public
*/
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;
};
/**
* Adds a session id we should prevent relaying messages to (flag).
*
* @api public
*/
SocketNamespace.prototype.except = function (id) {
this.flags.exceptions.push(id);
return this;
};
/**
* Sets the default flags.
*
* @api private
*/
SocketNamespace.prototype.setFlags = function () {
this.flags = {
endpoint: this.name
, exceptions: []
};
return this;
};
/**
* Sends out a packet.
*
* @api private
*/
SocketNamespace.prototype.packet = function (packet) {
packet.endpoint = this.name;
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
});
};
/**
* 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
*/
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];
}
};
/**
* Performs authentication.
*
* @param Object client request data
* @api private
*/
SocketNamespace.prototype.authorize = function (data, fn) {
if (this.auth) {
var self = this;
this.auth.call(this, data, function (err, authorized) {
self.log.debug('client ' +
(authorized ? '' : 'un') + 'authorized for ' + self.name);
fn(err, authorized);
});
} else {
this.log.debug('client authorized for ' + this.name);
fn(null, true);
}
return this;
};
/**
* Handles a packet.
*
* @api private
*/
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
.tests/networking/node_modules/socket.io/lib/parser.js generated vendored 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)];
}
};

View file

@ -0,0 +1,136 @@
/*!
* 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.5';
/**
* 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 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');

369
.tests/networking/node_modules/socket.io/lib/socket.js generated vendored Normal file
View file

@ -0,0 +1,369 @@
/*!
* socket.io-node
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var parser = require('./parser')
, util = require('./util')
, EventEmitter = process.EventEmitter
/**
* Export the constructor.
*/
exports = module.exports = Socket;
/**
* 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
*/
function Socket (manager, id, nsp, readable) {
this.id = id;
this.namespace = nsp;
this.manager = manager;
this.disconnected = false;
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.
*/
Socket.prototype.__proto__ = EventEmitter.prototype;
/**
* Accessor shortcut for the handshake data
*
* @api private
*/
Socket.prototype.__defineGetter__('handshake', function () {
return this.manager.handshaken[this.id];
});
/**
* 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.
*
* @api public
*/
Socket.prototype.__defineGetter__('json', function () {
this.flags.json = true;
return this;
});
/**
* 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);
}
} else {
this.packet({type: 'disconnect'});
this.manager.onLeave(this.id, this.namespace.name);
this.$emit('disconnect', 'booted');
}
}
return this;
};
/**
* Send a message.
*
* @api public
*/
Socket.prototype.send = function (data, fn) {
var packet = {
type: this.flags.json ? 'json' : 'message'
, data: data
};
if (fn) {
packet.id = ++this.ackPackets;
packet.ack = true;
this.acks[packet.id] = fn;
}
return this.packet(packet);
};
/**
* Original emit function.
*
* @api private
*/
Socket.prototype.$emit = EventEmitter.prototype.emit;
/**
* Emit override for custom events.
*
* @api public
*/
Socket.prototype.emit = function (ev) {
if (ev == 'newListener') {
return this.$emit.apply(this, arguments);
}
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
.tests/networking/node_modules/socket.io/lib/static.js generated vendored 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('exit', 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
.tests/networking/node_modules/socket.io/lib/store.js generated vendored 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;
};

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;
};

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;
};

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,106 @@
/*!
* 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 () {
server = require('policyfile').createServer({
log: function(msg){
manager.log.info(msg.toLowerCase());
}
}, 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();
}
});
// 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,82 @@
/*!
* 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) {
data = '<script>_(' + JSON.stringify(data) + ');</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);
};

View file

@ -0,0 +1,119 @@
/*!
* 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) {
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 {
this.response = req.res;
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));
};

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,360 @@
/*!
* 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']
, location = ((this.manager.settings['match origin protocol'] ?
origin.match(/^https/) : this.socket.encrypted) ?
'wss' : 'ws')
+ '://' + this.req.headers.host + this.req.url
, waitingForNonce = false;
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
.tests/networking/node_modules/socket.io/lib/util.js generated vendored 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

@ -0,0 +1 @@
node_modules

View file

@ -0,0 +1,19 @@
Copyright (c) 2011 Arnout Kazemier,3rd-Eden
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,7 @@
doc:
dox --title "FlashPolicyFileServer" lib/* > doc/index.html
test:
expresso -I lib $(TESTFLAGS) tests/*.test.js
.PHONY: test doc

View file

@ -0,0 +1,98 @@
## LOL, WUT?
It basically allows you to allow or disallow Flash Player sockets from accessing your site.
## Installation
```bash
npm install policyfile
```
## Usage
The server is based on the regular and know `net` and `http` server patterns. So it you can just listen
for all the events that a `net` based server emits etc. But there is one extra event, the `connect_failed`
event. This event is triggered when we are unable to listen on the supplied port number.
### createServer
Creates a new server instance and accepts 2 optional arguments:
- `options` **Object** Options to configure the server instance
- `log` **Boolean** Enable logging to STDOUT and STDERR (defaults to true)
- `origins` **Array** An Array of origins that are allowed by the server (defaults to *:*)
```js
var pf = require('policyfile');
pf.createServer();
pf.listen();
```
#### server.listen
Start listening on the server and it takes 3 optional arguments
- `port` **Number** On which port number should we listen? (defaults to 843, which is the first port number the FlashPlayer checks)
- `server` **Server** A http server, if we are unable to accept requests or run the server we can also answer the policy requests inline over the supplied HTTP server.
- `callback` **Function** A callback function that is called when listening to the server was successful.
```js
var pf = require('policyfile');
pf.createServer();
pf.listen(1337, function(){
console.log(':3 yay')
});
```
Changing port numbers can be handy if you do not want to run your server as root and have port 843 forward to a non root port number (aka a number above 1024).
```js
var pf = require('policyfile')
, http = require('http');
server = http.createServer(function(q,r){r.writeHead(200);r.end('hello world')});
server.listen(80);
pf.createServer();
pf.listen(1337, server, function(){
console.log(':3 yay')
});
```
Support for serving inline requests over a existing HTTP connection as the FlashPlayer will first check port 843, but if it's unable to get a response there it will send a policy file request over port 80, which is usually your http server.
#### server.add
Adds more origins to the policy file you can add as many arguments as you like.
```js
var pf = require('policyfile');
pf.createServer(['google.com:80']);
pf.listen();
pf.add('blog.3rd-Eden.com:80', 'blog.3rd-Eden.com:8080'); // now has 3 origins
```
#### server.add
Adds more origins to the policy file you can add as many arguments as you like.
```js
var pf = require('policyfile');
pf.createServer(['blog.3rd-Eden.com:80', 'blog.3rd-Eden.com:8080']);
pf.listen();
pf.remove('blog.3rd-Eden.com:8080'); // only contains the :80 version now
```
#### server.close
Shuts down the server
```js
var pf = require('policyfile');
pf.createServer();
pf.listen();
pf.close(); // OH NVM.
```
## API
http://3rd-eden.com/FlashPolicyFileServer/
## Examples
See https://github.com/3rd-Eden/FlashPolicyFileServer/tree/master/examples for examples
## Licence
MIT see LICENSE file in the repository

View file

@ -0,0 +1,375 @@
<html>
<head>
<title>FlashPolicyFileServer</title>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<style>body {
margin: 0;
padding: 0;
font: 14px/1.5 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif;
color: #252519;
}
a {
color: #252519;
}
a:hover {
text-decoration: underline;
color: #19469D;
}
p {
margin: 12px 0;
}
h1, h2, h3 {
margin: 0;
padding: 0;
}
table#source {
width: 100%;
border-collapse: collapse;
}
table#source td:first-child {
padding: 30px 40px 30px 40px;
vertical-align: top;
}
table#source td:first-child,
table#source td:first-child pre {
width: 450px;
}
table#source td:last-child {
padding: 30px 0 30px 40px;
border-left: 1px solid #E5E5EE;
background: #F5F5FF;
}
table#source tr {
border-bottom: 1px solid #E5E5EE;
}
table#source tr.filename {
padding-top: 40px;
border-top: 1px solid #E5E5EE;
}
table#source tr.filename td:first-child {
text-transform: capitalize;
}
table#source tr.filename td:last-child {
font-size: 12px;
}
table#source tr.filename h2 {
margin: 0;
padding: 0;
cursor: pointer;
}
table#source tr.code h1,
table#source tr.code h2,
table#source tr.code h3 {
margin-top: 30px;
font-family: "Lucida Grande", "Helvetica Nueue", Arial, sans-serif;
font-size: 18px;
}
table#source tr.code h2 {
font-size: 16px;
}
table#source tr.code h3 {
font-size: 14px;
}
table#source tr.code ul {
margin: 15px 0 15px 35px;
padding: 0;
}
table#source tr.code ul li {
margin: 0;
padding: 1px 0;
}
table#source tr.code ul li p {
margin: 0;
padding: 0;
}
table#source tr.code td:first-child pre {
padding: 20px;
}
#ribbon {
position: fixed;
top: 0;
right: 0;
}
code .string { color: #219161; }
code .regexp { color: #219161; }
code .keyword { color: #954121; }
code .number { color: #19469D; }
code .comment { color: #bbb; }
code .this { color: #19469D; }</style>
<script>
$(function(){
$('tr.code').hide();
$('tr.filename').toggle(function(){
$(this).nextUntil('.filename').fadeIn();
}, function(){
$(this).nextUntil('.filename').fadeOut();
});
});
</script>
</head>
<body>
<table id="source"><tbody><tr><td><h1>FlashPolicyFileServer</h1></td><td></td></tr><tr class="filename"><td><h2 id="lib/server.js"><a href="#">server</a></h2></td><td>lib/server.js</td></tr><tr class="code">
<td class="docs">
<p>Module dependencies and cached references.
</p>
</td>
<td class="code">
<pre><code><span class="keyword">var</span> <span class="variable">slice</span> = <span class="class">Array</span>.<span class="variable">prototype</span>.<span class="variable">slice</span>
, <span class="variable">net</span> = <span class="variable">require</span>(<span class="string">'net'</span>);</code></pre>
</td>
</tr>
<tr class="code">
<td class="docs">
<p>The server that does the Policy File severing</p>
<h2>Options</h2>
<ul><li><code>log</code> false or a function that can output log information, defaults to console.log?</li></ul>
<h2></h2>
<ul><li><p><strong>param</strong>: <em>Object</em> options Options to customize the servers functionality.</p></li><li><p><strong>param</strong>: <em>Array</em> origins The origins that are allowed on this server, defaults to <code>*:*</code>.</p></li><li><p><strong>api</strong>: <em>public</em></p></li></ul>
</td>
<td class="code">
<pre><code><span class="keyword">function</span> <span class="class">Server</span>(<span class="variable">options</span>, <span class="variable">origins</span>){
<span class="keyword">var</span> <span class="variable">me</span> = <span class="this">this</span>;
<span class="this">this</span>.<span class="variable">origins</span> = <span class="variable">origins</span> || [<span class="string">'*:*'</span>];
<span class="this">this</span>.<span class="variable">port</span> = <span class="number integer">843</span>;
<span class="this">this</span>.<span class="variable">log</span> = <span class="variable">console</span>.<span class="variable">log</span>;
<span class="comment">// merge `this` with the options</span>
<span class="class">Object</span>.<span class="variable">keys</span>(<span class="variable">options</span>).<span class="variable">forEach</span>(<span class="keyword">function</span>(<span class="variable">key</span>){
<span class="variable">me</span>[<span class="variable">key</span>] &<span class="variable">amp</span>;&<span class="variable">amp</span>; (<span class="variable">me</span>[<span class="variable">key</span>] = <span class="variable">options</span>[<span class="variable">key</span>])
});
<span class="comment">// create the net server</span>
<span class="this">this</span>.<span class="variable">socket</span> = <span class="variable">net</span>.<span class="variable">createServer</span>(<span class="keyword">function</span> <span class="variable">createServer</span>(<span class="variable">socket</span>){
<span class="variable">socket</span>.<span class="variable">on</span>(<span class="string">'error'</span>, <span class="keyword">function</span> <span class="variable">socketError</span>(){ <span class="variable">me</span>.<span class="variable">responder</span>.<span class="variable">call</span>(<span class="variable">me</span>, <span class="variable">socket</span>) });
<span class="variable">me</span>.<span class="variable">responder</span>.<span class="variable">call</span>(<span class="variable">me</span>, <span class="variable">socket</span>);
});
<span class="comment">// Listen for errors as the port might be blocked because we do not have root priv.</span>
<span class="this">this</span>.<span class="variable">socket</span>.<span class="variable">on</span>(<span class="string">'error'</span>, <span class="keyword">function</span> <span class="variable">serverError</span>(<span class="variable">err</span>){
<span class="comment">// Special and common case error handling</span>
<span class="keyword">if</span> (<span class="variable">err</span>.<span class="variable">errno</span> == <span class="number integer">13</span>){
<span class="variable">me</span>.<span class="variable">log</span> &<span class="variable">amp</span>;&<span class="variable">amp</span>; <span class="variable">me</span>.<span class="variable">log</span>(
<span class="string">'Unable to listen to port `'</span> + <span class="variable">me</span>.<span class="variable">port</span> + <span class="string">'` as your Node.js instance does not have root privileges. '</span> +
(
<span class="variable">me</span>.<span class="variable">server</span>
? <span class="string">'The Flash Policy file will now be served inline over the supplied HTTP server, Flash Policy files request will suffer.'</span>
: <span class="string">'No fallback server supplied.'</span>
)
);
<span class="variable">me</span>.<span class="variable">socket</span>.<span class="variable">removeAllListeners</span>();
<span class="keyword">delete</span> <span class="variable">me</span>.<span class="variable">socket</span>;
<span class="variable">me</span>.<span class="variable">emit</span>(<span class="string">'connect_failed'</span>, <span class="variable">err</span>);
} <span class="keyword">else</span> {
<span class="variable">me</span>.<span class="variable">log</span> &<span class="variable">amp</span>;&<span class="variable">amp</span>; <span class="variable">me</span>.<span class="variable">log</span>(<span class="string">'FlashPolicyFileServer received a error event:\n'</span> + (<span class="variable">err</span>.<span class="variable">message</span> ? <span class="variable">err</span>.<span class="variable">message</span> : <span class="variable">err</span>));
}
});
<span class="this">this</span>.<span class="variable">socket</span>.<span class="variable">on</span>(<span class="string">'timeout'</span>, <span class="keyword">function</span> <span class="variable">serverTimeout</span>(){});
<span class="this">this</span>.<span class="variable">socket</span>.<span class="variable">on</span>(<span class="string">'close'</span>, <span class="keyword">function</span> <span class="variable">serverClosed</span>(<span class="variable">err</span>){
<span class="variable">err</span> &<span class="variable">amp</span>;&<span class="variable">amp</span>; <span class="variable">me</span>.<span class="variable">log</span> &<span class="variable">amp</span>;&<span class="variable">amp</span>; <span class="variable">me</span>.<span class="variable">log</span>(<span class="string">'Server closing due to an error: \n'</span> + (<span class="variable">err</span>.<span class="variable">message</span> ? <span class="variable">err</span>.<span class="variable">message</span> : <span class="variable">err</span>));
<span class="keyword">if</span> (<span class="variable">me</span>.<span class="variable">server</span>){
<span class="comment">// not online anymore</span>
<span class="keyword">delete</span> <span class="variable">me</span>.<span class="variable">server</span>.<span class="variable">online</span>;
<span class="comment">// Remove the inline policy listener if we close down</span>
<span class="comment">// but only when the server was `online` (see listen prototype)</span>
<span class="keyword">if</span>( <span class="variable">me</span>.<span class="variable">server</span>[<span class="string">'@'</span>] &<span class="variable">amp</span>;&<span class="variable">amp</span>; <span class="variable">me</span>.<span class="variable">server</span>.<span class="variable">online</span>){
<span class="variable">me</span>.<span class="variable">server</span>.<span class="variable">removeListener</span>(<span class="string">'connection'</span>, <span class="variable">me</span>.<span class="variable">server</span>[<span class="string">'@'</span>]);
}
}
<span class="variable">me</span>.<span class="variable">log</span> &<span class="variable">amp</span>;&<span class="variable">amp</span>; <span class="variable">me</span>.<span class="variable">log</span>(<span class="string">'Shutting down FlashPolicyFileServer'</span>);
});
<span class="comment">// Compile the initial `buffer`</span>
<span class="this">this</span>.<span class="variable">compile</span>();
}</code></pre>
</td>
</tr>
<tr class="code">
<td class="docs">
<p>Start listening for requests</p>
<h2></h2>
<ul><li><p><strong>param</strong>: <em>Number</em> port The port number it should be listening to.</p></li><li><p><strong>param</strong>: <em>Server</em> server A HTTP server instance, this will be used to listen for inline requests</p></li><li><p><strong>param</strong>: <em>Function</em> cb The callback needs to be called once server is ready</p></li><li><p><strong>api</strong>: <em>public</em></p></li></ul>
</td>
<td class="code">
<pre><code><span class="class">Server</span>.<span class="variable">prototype</span>.<span class="variable">listen</span> = <span class="keyword">function</span> <span class="variable">listen</span>(<span class="variable">port</span>, <span class="variable">server</span>, <span class="variable">cb</span>){
<span class="keyword">var</span> <span class="variable">me</span> = <span class="this">this</span>
, <span class="variable">args</span> = <span class="variable">slice</span>.<span class="variable">call</span>(<span class="variable">arguments</span>, <span class="number integer">0</span>)
, <span class="variable">callback</span>;
<span class="comment">// assign the correct vars, for flexible arguments</span>
<span class="variable">args</span>.<span class="variable">forEach</span>(<span class="keyword">function</span> <span class="variable">args</span>(<span class="variable">arg</span>){
<span class="keyword">var</span> <span class="variable">type</span> = <span class="keyword">typeof</span> <span class="variable">arg</span>;
<span class="keyword">if</span> (<span class="variable">type</span> === <span class="string">'number'</span>) <span class="variable">me</span>.<span class="variable">port</span> = <span class="variable">arg</span>;
<span class="keyword">if</span> (<span class="variable">type</span> === <span class="string">'function'</span>) <span class="variable">callback</span> = <span class="variable">arg</span>;
<span class="keyword">if</span> (<span class="variable">type</span> === <span class="string">'object'</span>) <span class="variable">me</span>.<span class="variable">server</span> = <span class="variable">arg</span>;
});
<span class="keyword">if</span> (<span class="this">this</span>.<span class="variable">server</span>){
<span class="comment">// no one in their right mind would ever create a `@` prototype, so Im just gonna store</span>
<span class="comment">// my function on the server, so I can remove it later again once the server(s) closes</span>
<span class="this">this</span>.<span class="variable">server</span>[<span class="string">'@'</span>] = <span class="keyword">function</span> <span class="variable">connection</span>(<span class="variable">socket</span>){
<span class="variable">socket</span>.<span class="variable">once</span>(<span class="string">'data'</span>, <span class="keyword">function</span> <span class="variable">requestData</span>(<span class="variable">data</span>){
<span class="comment">// if it's a Flash policy request, and we can write to the </span>
<span class="keyword">if</span> (
<span class="variable">data</span>
&<span class="variable">amp</span>;&<span class="variable">amp</span>; <span class="variable">data</span>[<span class="number integer">0</span>] === <span class="number integer">60</span>
&<span class="variable">amp</span>;&<span class="variable">amp</span>; <span class="variable">data</span>.<span class="variable">toString</span>() === <span class="string">'&lt;policy-file-request/&gt;\0'</span>
&<span class="variable">amp</span>;&<span class="variable">amp</span>; <span class="variable">socket</span>
&<span class="variable">amp</span>;&<span class="variable">amp</span>; (<span class="variable">socket</span>.<span class="variable">readyState</span> === <span class="string">'open'</span> || <span class="variable">socket</span>.<span class="variable">readyState</span> === <span class="string">'writeOnly'</span>)
){
<span class="comment">// send the buffer</span>
<span class="variable">socket</span>.<span class="variable">end</span>(<span class="variable">me</span>.<span class="variable">buffer</span>);
}
});
};
<span class="comment">// attach it</span>
<span class="this">this</span>.<span class="variable">server</span>.<span class="variable">on</span>(<span class="string">'connection'</span>, <span class="this">this</span>.<span class="variable">server</span>[<span class="string">'@'</span>]);
}
<span class="comment">// We add a callback method, so we can set a flag for when the server is `enabled` or `online`.</span>
<span class="comment">// this flag is needed because if a error occurs and the we cannot boot up the server the</span>
<span class="comment">// fallback functionality should not be removed during the `close` event</span>
<span class="this">this</span>.<span class="variable">socket</span>.<span class="variable">listen</span>(<span class="this">this</span>.<span class="variable">port</span>, <span class="keyword">function</span> <span class="variable">serverListening</span>(){
<span class="variable">me</span>.<span class="variable">socket</span>.<span class="variable">online</span> = <span class="variable">true</span>;
<span class="keyword">if</span> (<span class="variable">callback</span>) <span class="variable">callback</span>(), <span class="variable">callback</span> = <span class="variable">undefined</span>;
});
<span class="keyword">return</span> <span class="this">this</span>;
};</code></pre>
</td>
</tr>
<tr class="code">
<td class="docs">
<p>Adds a new origin to the Flash Policy File.</p>
<h2></h2>
<ul><li><p><strong>param</strong>: <em>Arguments</em> The origins that need to be added.</p></li><li><p><strong>api</strong>: <em>public</em></p></li></ul>
</td>
<td class="code">
<pre><code><span class="class">Server</span>.<span class="variable">prototype</span>.<span class="variable">add</span> = <span class="keyword">function</span> <span class="variable">add</span>(){
<span class="keyword">var</span> <span class="variable">args</span> = <span class="variable">slice</span>.<span class="variable">call</span>(<span class="variable">arguments</span>, <span class="number integer">0</span>)
, <span class="variable">i</span> = <span class="variable">args</span>.<span class="variable">length</span>;
<span class="comment">// flag duplicates</span>
<span class="keyword">while</span> (<span class="variable">i</span>--){
<span class="keyword">if</span> (<span class="this">this</span>.<span class="variable">origins</span>.<span class="variable">indexOf</span>(<span class="variable">args</span>[<span class="variable">i</span>]) &<span class="variable">gt</span>;= <span class="number integer">0</span>){
<span class="variable">args</span>[<span class="variable">i</span>] = <span class="keyword">null</span>;
}
}
<span class="comment">// Add all the arguments to the array</span>
<span class="comment">// but first we want to remove all `falsy` values from the args</span>
<span class="class">Array</span>.<span class="variable">prototype</span>.<span class="variable">push</span>.<span class="variable">apply</span>(
<span class="this">this</span>.<span class="variable">origins</span>
, <span class="variable">args</span>.<span class="variable">filter</span>(<span class="keyword">function</span>(<span class="variable">value</span>){ <span class="keyword">return</span> !!<span class="variable">value</span> })
);
<span class="this">this</span>.<span class="variable">compile</span>();
<span class="keyword">return</span> <span class="this">this</span>;
};</code></pre>
</td>
</tr>
<tr class="code">
<td class="docs">
<p>Removes a origin from the Flash Policy File.</p>
<h2></h2>
<ul><li><p><strong>param</strong>: <em>String</em> origin The origin that needs to be removed from the server</p></li><li><p><strong>api</strong>: <em>public</em></p></li></ul>
</td>
<td class="code">
<pre><code><span class="class">Server</span>.<span class="variable">prototype</span>.<span class="variable">remove</span> = <span class="keyword">function</span> <span class="variable">remove</span>(<span class="variable">origin</span>){
<span class="keyword">var</span> <span class="variable">position</span> = <span class="this">this</span>.<span class="variable">origins</span>.<span class="variable">indexOf</span>(<span class="variable">origin</span>);
<span class="comment">// only remove and recompile if we have a match</span>
<span class="keyword">if</span> (<span class="variable">position</span> &<span class="variable">gt</span>; <span class="number integer">0</span>){
<span class="this">this</span>.<span class="variable">origins</span>.<span class="variable">splice</span>(<span class="variable">position</span>,<span class="number integer">1</span>);
<span class="this">this</span>.<span class="variable">compile</span>();
}
<span class="keyword">return</span> <span class="this">this</span>;
};</code></pre>
</td>
</tr>
<tr class="code">
<td class="docs">
<p>Closes and cleans up the server</p>
<ul><li><p><strong>api</strong>: <em>public</em></p></li></ul>
</td>
<td class="code">
<pre><code><span class="class">Server</span>.<span class="variable">prototype</span>.<span class="variable">close</span> = <span class="keyword">function</span> <span class="variable">close</span>(){
<span class="this">this</span>.<span class="variable">socket</span>.<span class="variable">removeAllListeners</span>();
<span class="this">this</span>.<span class="variable">socket</span>.<span class="variable">close</span>();
<span class="keyword">return</span> <span class="this">this</span>;
};</code></pre>
</td>
</tr>
<tr class="code">
<td class="docs">
<p>Proxy the event listener requests to the created Net server
</p>
</td>
<td class="code">
<pre><code><span class="class">Object</span>.<span class="variable">keys</span>(<span class="variable">process</span>.<span class="class">EventEmitter</span>.<span class="variable">prototype</span>).<span class="variable">forEach</span>(<span class="keyword">function</span> <span class="variable">proxy</span>(<span class="variable">key</span>){
<span class="class">Server</span>.<span class="variable">prototype</span>[<span class="variable">key</span>] = <span class="class">Server</span>.<span class="variable">prototype</span>[<span class="variable">key</span>] || <span class="keyword">function</span> (){
<span class="keyword">if</span> (<span class="this">this</span>.<span class="variable">socket</span>) <span class="this">this</span>.<span class="variable">socket</span>[<span class="variable">key</span>].<span class="variable">apply</span>(<span class="this">this</span>.<span class="variable">socket</span>, <span class="variable">arguments</span>);
<span class="keyword">return</span> <span class="this">this</span>;
};
});</code></pre>
</td>
</tr>
<tr class="code">
<td class="docs">
<p>Creates a new server instance.</p>
<h2></h2>
<ul><li><p><strong>param</strong>: <em>Object</em> options A options object to override the default config</p></li><li><p><strong>param</strong>: <em>Array</em> origins The origins that should be allowed by the server</p></li><li><p><strong>api</strong>: <em>public</em></p></li></ul>
</td>
<td class="code">
<pre><code><span class="variable">exports</span>.<span class="variable">createServer</span> = <span class="keyword">function</span> <span class="variable">createServer</span>(<span class="variable">options</span>, <span class="variable">origins</span>){
<span class="variable">origins</span> = <span class="class">Array</span>.<span class="variable">isArray</span>(<span class="variable">origins</span>) ? <span class="variable">origins</span> : (<span class="class">Array</span>.<span class="variable">isArray</span>(<span class="variable">options</span>) ? <span class="variable">options</span> : <span class="variable">false</span>);
<span class="variable">options</span> = !<span class="class">Array</span>.<span class="variable">isArray</span>(<span class="variable">options</span>) &<span class="variable">amp</span>;&<span class="variable">amp</span>; <span class="variable">options</span> ? <span class="variable">options</span> : {};
<span class="keyword">return</span> <span class="keyword">new</span> <span class="class">Server</span>(<span class="variable">options</span>, <span class="variable">origins</span>);
};</code></pre>
</td>
</tr>
<tr class="code">
<td class="docs">
<p>Provide a hook to the original server, so it can be extended if needed.
</p>
</td>
<td class="code">
<pre><code><span class="variable">exports</span>.<span class="class">Server</span> = <span class="class">Server</span>;</code></pre>
</td>
</tr>
<tr class="code">
<td class="docs">
<p>Module version
</p>
</td>
<td class="code">
<pre><code><span class="variable">exports</span>.<span class="variable">version</span> = <span class="string">'0.0.2'</span>;
</code></pre>
</td>
</tr> </body>
</html></tbody></table>

View file

@ -0,0 +1,8 @@
var http = require('http')
, fspfs = require('../');
var server = http.createServer(function(q,r){ r.writeHead(200); r.end(':3') })
, flash = fspfs.createServer();
server.listen(8080);
flash.listen(8081,server);

View file

@ -0,0 +1,5 @@
var http = require('http')
, fspfs = require('../');
var flash = fspfs.createServer();
flash.listen();

View file

@ -0,0 +1 @@
module.exports = require('./lib/server.js');

View file

@ -0,0 +1,289 @@
/**
* Module dependencies and cached references.
*/
var slice = Array.prototype.slice
, net = require('net');
/**
* The server that does the Policy File severing
*
* Options:
* - `log` false or a function that can output log information, defaults to console.log?
*
* @param {Object} options Options to customize the servers functionality.
* @param {Array} origins The origins that are allowed on this server, defaults to `*:*`.
* @api public
*/
function Server (options, origins) {
var me = this;
this.origins = origins || ['*:*'];
this.port = 843;
this.log = console.log;
// merge `this` with the options
Object.keys(options).forEach(function (key) {
me[key] && (me[key] = options[key])
});
// create the net server
this.socket = net.createServer(function createServer (socket) {
socket.on('error', function socketError () {
me.responder.call(me, socket);
});
me.responder.call(me, socket);
});
// Listen for errors as the port might be blocked because we do not have root priv.
this.socket.on('error', function serverError (err) {
// Special and common case error handling
if (err.errno == 13) {
me.log && me.log(
'Unable to listen to port `' + me.port + '` as your Node.js instance does not have root privileges. ' +
(
me.server
? 'The Flash Policy File requests will only be served inline over the supplied HTTP server. Inline serving is slower than a dedicated server instance.'
: 'No fallback server supplied, we will be unable to answer Flash Policy File requests.'
)
);
me.emit('connect_failed', err);
me.socket.removeAllListeners();
delete me.socket;
} else {
me.log && me.log('FlashPolicyFileServer received an error event:\n' + (err.message ? err.message : err));
}
});
this.socket.on('timeout', function serverTimeout () {});
this.socket.on('close', function serverClosed (err) {
err && me.log && me.log('Server closing due to an error: \n' + (err.message ? err.message : err));
if (me.server) {
// Remove the inline policy listener if we close down
// but only when the server was `online` (see listen prototype)
if (me.server['@'] && me.server.online) {
me.server.removeListener('connection', me.server['@']);
}
// not online anymore
delete me.server.online;
}
});
// Compile the initial `buffer`
this.compile();
}
/**
* Start listening for requests
*
* @param {Number} port The port number it should be listening to.
* @param {Server} server A HTTP server instance, this will be used to listen for inline requests
* @param {Function} cb The callback needs to be called once server is ready
* @api public
*/
Server.prototype.listen = function listen (port, server, cb){
var me = this
, args = slice.call(arguments, 0)
, callback;
// assign the correct vars, for flexible arguments
args.forEach(function args (arg){
var type = typeof arg;
if (type === 'number') me.port = arg;
if (type === 'function') callback = arg;
if (type === 'object') me.server = arg;
});
if (this.server) {
// no one in their right mind would ever create a `@` prototype, so Im just gonna store
// my function on the server, so I can remove it later again once the server(s) closes
this.server['@'] = function connection (socket) {
socket.once('data', function requestData (data) {
// if it's a Flash policy request, and we can write to the
if (
data
&& data[0] === 60
&& data.toString() === '<policy-file-request/>\0'
&& socket
&& (socket.readyState === 'open' || socket.readyState === 'writeOnly')
){
// send the buffer
try {
socket.end(me.buffer);
} catch (e) {}
}
});
};
// attach it
this.server.on('connection', this.server['@']);
}
// We add a callback method, so we can set a flag for when the server is `enabled` or `online`.
// this flag is needed because if a error occurs and the we cannot boot up the server the
// fallback functionality should not be removed during the `close` event
this.port >= 0 && this.socket.listen(this.port, function serverListening () {
me.socket.online = true;
if (callback) {
callback.call(me);
callback = undefined;
}
});
return this;
};
/**
* Responds to socket connects and writes the compile policy file.
*
* @param {net.Socket} socket The socket that needs to receive the message
* @api private
*/
Server.prototype.responder = function responder (socket){
if (socket && socket.readyState == 'open' && socket.end) {
try {
socket.end(this.buffer);
} catch (e) {}
}
};
/**
* Compiles the supplied origins to a Flash Policy File format and stores it in a Node.js Buffer
* this way it can be send over the wire without any performance loss.
*
* @api private
*/
Server.prototype.compile = function compile (){
var xml = [
'<?xml version="1.0"?>'
, '<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">'
, '<cross-domain-policy>'
];
// add the allow access element
this.origins.forEach(function origin (origin){
var parts = origin.split(':');
xml.push('<allow-access-from domain="' + parts[0] + '" to-ports="'+ parts[1] +'"/>');
});
xml.push('</cross-domain-policy>');
// store the result in a buffer so we don't have to re-generate it all the time
this.buffer = new Buffer(xml.join(''), 'utf8');
return this;
};
/**
* Adds a new origin to the Flash Policy File.
*
* @param {Arguments} The origins that need to be added.
* @api public
*/
Server.prototype.add = function add(){
var args = slice.call(arguments, 0)
, i = args.length;
// flag duplicates
while (i--) {
if (this.origins.indexOf(args[i]) >= 0){
args[i] = null;
}
}
// Add all the arguments to the array
// but first we want to remove all `falsy` values from the args
Array.prototype.push.apply(
this.origins
, args.filter(function filter (value) {
return !!value;
})
);
this.compile();
return this;
};
/**
* Removes a origin from the Flash Policy File.
*
* @param {String} origin The origin that needs to be removed from the server
* @api public
*/
Server.prototype.remove = function remove (origin){
var position = this.origins.indexOf(origin);
// only remove and recompile if we have a match
if (position > 0) {
this.origins.splice(position,1);
this.compile();
}
return this;
};
/**
* Closes and cleans up the server
*
* @api public
*/
Server.prototype.close = function close () {
this.socket.removeAllListeners();
this.socket.close();
return this;
};
/**
* Proxy the event listener requests to the created Net server
*/
Object.keys(process.EventEmitter.prototype).forEach(function proxy (key){
Server.prototype[key] = Server.prototype[key] || function () {
if (this.socket) {
this.socket[key].apply(this.socket, arguments);
}
return this;
};
});
/**
* Creates a new server instance.
*
* @param {Object} options A options object to override the default config
* @param {Array} origins The origins that should be allowed by the server
* @api public
*/
exports.createServer = function createServer(options, origins){
origins = Array.isArray(origins) ? origins : (Array.isArray(options) ? options : false);
options = !Array.isArray(options) && options ? options : {};
return new Server(options, origins);
};
/**
* Provide a hook to the original server, so it can be extended if needed.
*/
exports.Server = Server;
/**
* Module version
*/
exports.version = '0.0.4';

View file

@ -0,0 +1,55 @@
{
"name": "policyfile",
"version": "0.0.4",
"author": {
"name": "Arnout Kazemier"
},
"description": "Flash Socket Policy File Server. A server to respond to Flash Socket Policy requests, both inline and through a dedicated server instance.",
"main": "index",
"keywords": [
"flash",
"socket",
"policy",
"file",
"server",
"Flash Socket Policy File Server",
"cross domain"
],
"directories": {
"lib": "./lib"
},
"maintainers": [
{
"name": "Arnout Kazemier",
"email": "info@3rd-Eden.com",
"url": "http://blog.3rd-Eden.com"
}
],
"licenses": [
{
"type": "MIT",
"url": "https://github.com/3rd-Eden/FlashPolicyFileServer/blob/master/LICENSE"
}
],
"repositories": [
{
"type": "git",
"url": "https://github.com/3rd-Eden/FlashPolicyFileServer.git"
}
],
"_id": "policyfile@0.0.4",
"dependencies": {},
"devDependencies": {},
"optionalDependencies": {},
"engines": {
"node": "*"
},
"_engineSupported": true,
"_npmVersion": "1.1.4",
"_nodeVersion": "v0.6.12",
"_defaultsLoaded": true,
"dist": {
"shasum": "6abed755dc4c523bb66e1ad141721109483381d0"
},
"_from": "policyfile@0.0.4"
}

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-----

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-----

View file

@ -0,0 +1,231 @@
var fspfs = require('../')
, fs = require('fs')
, http = require('http')
, https = require('https')
, net = require('net')
, should = require('should')
, assert = require('assert');
module.exports = {
// Library version should be Semver compatible
'Library version': function(){
fspfs.version.should.match(/^\d+\.\d+\.\d+$/);
}
// Creating a server instace should not cause any problems
// either using the new Server or createServer method.
, 'Create Server instance': function(){
var server = fspfs.createServer()
, server2 = new fspfs.Server({log:false}, ['blog.3rd-Eden.com:1337']);
// server 2 options test
server2.log.should.be.false;
server2.origins.length.should.equal(1);
server2.origins[0].should.equal('blog.3rd-Eden.com:1337');
// server defaults
(typeof server.log).should.be.equal('function');
server.origins.length.should.equal(1);
server.origins[0].should.equal('*:*');
// instance checking, sanity check
assert.ok(server instanceof fspfs.Server);
assert.ok(!!server.buffer);
// more options testing
server = fspfs.createServer(['blog.3rd-Eden.com:80']);
server.origins.length.should.equal(1);
server.origins[0].should.equal('blog.3rd-Eden.com:80');
server = fspfs.createServer({log:false},['blog.3rd-Eden.com:80']);
server.log.should.be.false;
server.origins.length.should.equal(1);
server.origins[0].should.equal('blog.3rd-Eden.com:80');
}
, 'Add origin': function(){
var server = fspfs.createServer();
server.add('google.com:80', 'blog.3rd-Eden.com:1337');
server.origins.length.should.equal(3);
server.origins.indexOf('google.com:80').should.be.above(0);
// don't allow duplicates
server.add('google.com:80', 'google.com:80');
var i = server.origins.length
, count = 0;
while(i--){
if (server.origins[i] === 'google.com:80'){
count++;
}
}
count.should.equal(1);
}
, 'Remove origin': function(){
var server = fspfs.createServer();
server.add('google.com:80', 'blog.3rd-Eden.com:1337');
server.origins.length.should.equal(3);
server.remove('google.com:80');
server.origins.length.should.equal(2);
server.origins.indexOf('google.com:80').should.equal(-1);
}
, 'Buffer': function(){
var server = fspfs.createServer();
Buffer.isBuffer(server.buffer).should.be.true;
server.buffer.toString().indexOf('to-ports="*"').should.be.above(0);
server.buffer.toString().indexOf('domain="*"').should.be.above(0);
server.buffer.toString().indexOf('domain="google.com"').should.equal(-1);
// The buffers should be rebuild when new origins are added
server.add('google.com:80');
server.buffer.toString().indexOf('to-ports="80"').should.be.above(0);
server.buffer.toString().indexOf('domain="google.com"').should.be.above(0);
server.remove('google.com:80');
server.buffer.toString().indexOf('to-ports="80"').should.equal(-1);
server.buffer.toString().indexOf('domain="google.com"').should.equal(-1);
}
, 'Responder': function(){
var server = fspfs.createServer()
, calls = 0
// dummy socket to emulate a `real` socket
, dummySocket = {
readyState: 'open'
, end: function(buffer){
calls++;
Buffer.isBuffer(buffer).should.be.true;
buffer.toString().should.equal(server.buffer.toString());
}
};
server.responder(dummySocket);
calls.should.equal(1);
}
, 'Event proxy': function(){
var server = fspfs.createServer()
, calls = 0;
Object.keys(process.EventEmitter.prototype).forEach(function proxy(key){
assert.ok(!!server[key] && typeof server[key] === 'function');
});
// test if it works by calling a none default event
server.on('pew', function(){
calls++;
});
server.emit('pew');
calls.should.equal(1);
}
, 'inline response http': function(){
var port = 1335
, httpserver = http.createServer(function(q,r){r.writeHead(200);r.end(':3')})
, server = fspfs.createServer();
httpserver.listen(port, function(){
server.listen(port + 1, httpserver, function(){
var client = net.createConnection(port);
client.write('<policy-file-request/>\0');
client.on('error', function(err){
assert.ok(!err, err)
});
client.on('data', function(data){
var response = data.toString();
console.log(response);
response.indexOf('to-ports="*"').should.be.above(0);
response.indexOf('domain="*"').should.be.above(0);
response.indexOf('domain="google.com"').should.equal(-1);
// clean up
client.destroy();
server.close();
httpserver.close();
});
});
});
}
, 'server response': function(){
var port = 1340
, server = fspfs.createServer();
server.listen(port, function(){
var client = net.createConnection(port);
client.write('<policy-file-request/>\0');
client.on('error', function(err){
assert.ok(!err, err)
});
client.on('data', function(data){
var response = data.toString();
response.indexOf('to-ports="*"').should.be.above(0);
response.indexOf('domain="*"').should.be.above(0);
response.indexOf('domain="google.com"').should.equal(-1);
// clean up
client.destroy();
server.close();
});
});
}
, 'inline response https': function(){
var port = 1345
, ssl = {
key: fs.readFileSync(__dirname + '/ssl/ssl.private.key').toString()
, cert: fs.readFileSync(__dirname + '/ssl/ssl.crt').toString()
}
, httpserver = https.createServer(ssl, function(q,r){r.writeHead(200);r.end(':3')})
, server = fspfs.createServer();
httpserver.listen(port, function(){
server.listen(port + 1, httpserver, function(){
var client = net.createConnection(port);
client.write('<policy-file-request/>\0');
client.on('error', function(err){
assert.ok(!err, err)
});
client.on('data', function(data){
var response = data.toString();
response.indexOf('to-ports="*"').should.be.above(0);
response.indexOf('domain="*"').should.be.above(0);
response.indexOf('domain="google.com"').should.equal(-1);
// clean up
client.destroy();
server.close();
httpserver.close();
});
});
});
}
, 'connect_failed': function(){
var server = fspfs.createServer();
server.on('connect_failed', function(){
assert.ok(true);
});
server.listen(function(){
assert.ok(false, 'Run this test without root access');
server.close();
});
}
};

View file

@ -0,0 +1,567 @@
redis - a node.js redis client
===========================
This is a complete Redis client for node.js. It supports all Redis commands, including many recently added commands like EVAL from
experimental Redis server branches.
Install with:
npm install redis
Pieter Noordhuis has provided a binding to the official `hiredis` C library, which is non-blocking and fast. To use `hiredis`, do:
npm install hiredis redis
If `hiredis` is installed, `node_redis` will use it by default. Otherwise, a pure JavaScript parser will be used.
If you use `hiredis`, be sure to rebuild it whenever you upgrade your version of node. There are mysterious failures that can
happen between node and native code modules after a node upgrade.
## Usage
Simple example, included as `examples/simple.js`:
var redis = require("redis"),
client = redis.createClient();
client.on("error", function (err) {
console.log("Error " + err);
});
client.set("string key", "string val", redis.print);
client.hset("hash key", "hashtest 1", "some value", redis.print);
client.hset(["hash key", "hashtest 2", "some other value"], redis.print);
client.hkeys("hash key", function (err, replies) {
console.log(replies.length + " replies:");
replies.forEach(function (reply, i) {
console.log(" " + i + ": " + reply);
});
client.quit();
});
This will display:
mjr:~/work/node_redis (master)$ node example.js
Reply: OK
Reply: 0
Reply: 0
2 replies:
0: hashtest 1
1: hashtest 2
mjr:~/work/node_redis (master)$
## Performance
Here are typical results of `multi_bench.js` which is similar to `redis-benchmark` from the Redis distribution.
It uses 50 concurrent connections with no pipelining.
JavaScript parser:
PING: 20000 ops 42283.30 ops/sec 0/5/1.182
SET: 20000 ops 32948.93 ops/sec 1/7/1.515
GET: 20000 ops 28694.40 ops/sec 0/9/1.740
INCR: 20000 ops 39370.08 ops/sec 0/8/1.269
LPUSH: 20000 ops 36429.87 ops/sec 0/8/1.370
LRANGE (10 elements): 20000 ops 9891.20 ops/sec 1/9/5.048
LRANGE (100 elements): 20000 ops 1384.56 ops/sec 10/91/36.072
hiredis parser:
PING: 20000 ops 46189.38 ops/sec 1/4/1.082
SET: 20000 ops 41237.11 ops/sec 0/6/1.210
GET: 20000 ops 39682.54 ops/sec 1/7/1.257
INCR: 20000 ops 40080.16 ops/sec 0/8/1.242
LPUSH: 20000 ops 41152.26 ops/sec 0/3/1.212
LRANGE (10 elements): 20000 ops 36563.07 ops/sec 1/8/1.363
LRANGE (100 elements): 20000 ops 21834.06 ops/sec 0/9/2.287
The performance of `node_redis` improves dramatically with pipelining, which happens automatically in most normal programs.
### Sending Commands
Each Redis command is exposed as a function on the `client` object.
All functions take either take either an `args` Array plus optional `callback` Function or
a variable number of individual arguments followed by an optional callback.
Here is an example of passing an array of arguments and a callback:
client.mset(["test keys 1", "test val 1", "test keys 2", "test val 2"], function (err, res) {});
Here is that same call in the second style:
client.mset("test keys 1", "test val 1", "test keys 2", "test val 2", function (err, res) {});
Note that in either form the `callback` is optional:
client.set("some key", "some val");
client.set(["some other key", "some val"]);
For a list of Redis commands, see [Redis Command Reference](http://redis.io/commands)
The commands can be specified in uppercase or lowercase for convenience. `client.get()` is the same as `client.GET()`.
Minimal parsing is done on the replies. Commands that return a single line reply return JavaScript Strings,
integer replies return JavaScript Numbers, "bulk" replies return node Buffers, and "multi bulk" replies return a
JavaScript Array of node Buffers. `HGETALL` returns an Object with Buffers keyed by the hash keys.
# API
## Connection Events
`client` will emit some events about the state of the connection to the Redis server.
### "ready"
`client` will emit `ready` a connection is established to the Redis server and the server reports
that it is ready to receive commands. Commands issued before the `ready` event are queued,
then replayed just before this event is emitted.
### "connect"
`client` will emit `connect` at the same time as it emits `ready` unless `client.options.no_ready_check`
is set. If this options is set, `connect` will be emitted when the stream is connected, and then
you are free to try to send commands.
### "error"
`client` will emit `error` when encountering an error connecting to the Redis server.
Note that "error" is a special event type in node. If there are no listeners for an
"error" event, node will exit. This is usually what you want, but it can lead to some
cryptic error messages like this:
mjr:~/work/node_redis (master)$ node example.js
node.js:50
throw e;
^
Error: ECONNREFUSED, Connection refused
at IOWatcher.callback (net:870:22)
at node.js:607:9
Not very useful in diagnosing the problem, but if your program isn't ready to handle this,
it is probably the right thing to just exit.
`client` will also emit `error` if an exception is thrown inside of `node_redis` for whatever reason.
It would be nice to distinguish these two cases.
### "end"
`client` will emit `end` when an established Redis server connection has closed.
### "drain"
`client` will emit `drain` when the TCP connection to the Redis server has been buffering, but is now
writable. This event can be used to stream commands in to Redis and adapt to backpressure. Right now,
you need to check `client.command_queue.length` to decide when to reduce your send rate. Then you can
resume sending when you get `drain`.
### "idle"
`client` will emit `idle` when there are no outstanding commands that are awaiting a response.
## redis.createClient(port, host, options)
Create a new client connection. `port` defaults to `6379` and `host` defaults
to `127.0.0.1`. If you have `redis-server` running on the same computer as node, then the defaults for
port and host are probably fine. `options` in an object with the following possible properties:
* `parser`: which Redis protocol reply parser to use. Defaults to `hiredis` if that module is installed.
This may also be set to `javascript`.
* `return_buffers`: defaults to false. If set to `true`, then bulk data replies will be returned as node Buffer
objects instead of JavaScript Strings.
`createClient()` returns a `RedisClient` object that is named `client` in all of the examples here.
## client.auth(password, callback)
When connecting to Redis servers that require authentication, the `AUTH` command must be sent as the
first command after connecting. This can be tricky to coordinate with reconnections, the ready check,
etc. To make this easier, `client.auth()` stashes `password` and will send it after each connection,
including reconnections. `callback` is invoked only once, after the response to the very first
`AUTH` command sent.
## client.end()
Forcibly close the connection to the Redis server. Note that this does not wait until all replies have been parsed.
If you want to exit cleanly, call `client.quit()` to send the `QUIT` command after you have handled all replies.
This example closes the connection to the Redis server before the replies have been read. You probably don't
want to do this:
var redis = require("redis"),
client = redis.createClient();
client.set("foo_rand000000000000", "some fantastic value");
client.get("foo_rand000000000000", function (err, reply) {
console.log(reply.toString());
});
client.end();
`client.end()` is useful for timeout cases where something is stuck or taking too long and you want
to start over.
## Friendlier hash commands
Most Redis commands take a single String or an Array of Strings as arguments, and replies are sent back as a single String or an Array of Strings. When dealing with hash values, there are a couple of useful exceptions to this.
### client.hgetall(hash)
The reply from an HGETALL command will be converted into a JavaScript Object by `node_redis`. That way you can interact
with the responses using JavaScript syntax.
Example:
client.hmset("hosts", "mjr", "1", "another", "23", "home", "1234");
client.hgetall("hosts", function (err, obj) {
console.dir(obj);
});
Output:
{ mjr: '1', another: '23', home: '1234' }
### client.hmset(hash, obj, [callback])
Multiple values in a hash can be set by supplying an object:
client.HMSET(key2, {
"0123456789": "abcdefghij",
"some manner of key": "a type of value"
});
The properties and values of this Object will be set as keys and values in the Redis hash.
### client.hmset(hash, key1, val1, ... keyn, valn, [callback])
Multiple values may also be set by supplying a list:
client.HMSET(key1, "0123456789", "abcdefghij", "some manner of key", "a type of value");
## Publish / Subscribe
Here is a simple example of the API for publish / subscribe. This program opens two
client connections, subscribes to a channel on one of them, and publishes to that
channel on the other:
var redis = require("redis"),
client1 = redis.createClient(), client2 = redis.createClient(),
msg_count = 0;
client1.on("subscribe", function (channel, count) {
client2.publish("a nice channel", "I am sending a message.");
client2.publish("a nice channel", "I am sending a second message.");
client2.publish("a nice channel", "I am sending my last message.");
});
client1.on("message", function (channel, message) {
console.log("client1 channel " + channel + ": " + message);
msg_count += 1;
if (msg_count === 3) {
client1.unsubscribe();
client1.end();
client2.end();
}
});
client1.incr("did a thing");
client1.subscribe("a nice channel");
When a client issues a `SUBSCRIBE` or `PSUBSCRIBE`, that connection is put into "pub/sub" mode.
At that point, only commands that modify the subscription set are valid. When the subscription
set is empty, the connection is put back into regular mode.
If you need to send regular commands to Redis while in pub/sub mode, just open another connection.
## Pub / Sub Events
If a client has subscriptions active, it may emit these events:
### "message" (channel, message)
Client will emit `message` for every message received that matches an active subscription.
Listeners are passed the channel name as `channel` and the message Buffer as `message`.
### "pmessage" (pattern, channel, message)
Client will emit `pmessage` for every message received that matches an active subscription pattern.
Listeners are passed the original pattern used with `PSUBSCRIBE` as `pattern`, the sending channel
name as `channel`, and the message Buffer as `message`.
### "subscribe" (channel, count)
Client will emit `subscribe` in response to a `SUBSCRIBE` command. Listeners are passed the
channel name as `channel` and the new count of subscriptions for this client as `count`.
### "psubscribe" (pattern, count)
Client will emit `psubscribe` in response to a `PSUBSCRIBE` command. Listeners are passed the
original pattern as `pattern`, and the new count of subscriptions for this client as `count`.
### "unsubscribe" (channel, count)
Client will emit `unsubscribe` in response to a `UNSUBSCRIBE` command. Listeners are passed the
channel name as `channel` and the new count of subscriptions for this client as `count`. When
`count` is 0, this client has left pub/sub mode and no more pub/sub events will be emitted.
### "punsubscribe" (pattern, count)
Client will emit `punsubscribe` in response to a `PUNSUBSCRIBE` command. Listeners are passed the
channel name as `channel` and the new count of subscriptions for this client as `count`. When
`count` is 0, this client has left pub/sub mode and no more pub/sub events will be emitted.
## client.multi([commands])
`MULTI` commands are queued up until an `EXEC` is issued, and then all commands are run atomically by
Redis. The interface in `node_redis` is to return an individual `Multi` object by calling `client.multi()`.
var redis = require("./index"),
client = redis.createClient(), set_size = 20;
client.sadd("bigset", "a member");
client.sadd("bigset", "another member");
while (set_size > 0) {
client.sadd("bigset", "member " + set_size);
set_size -= 1;
}
// multi chain with an individual callback
client.multi()
.scard("bigset")
.smembers("bigset")
.keys("*", function (err, replies) {
client.mget(replies, redis.print);
})
.dbsize()
.exec(function (err, replies) {
console.log("MULTI got " + replies.length + " replies");
replies.forEach(function (reply, index) {
console.log("Reply " + index + ": " + reply.toString());
});
});
`client.multi()` is a constructor that returns a `Multi` object. `Multi` objects share all of the
same command methods as `client` objects do. Commands are queued up inside the `Multi` object
until `Multi.exec()` is invoked.
You can either chain together `MULTI` commands as in the above example, or you can queue individual
commands while still sending regular client command as in this example:
var redis = require("redis"),
client = redis.createClient(), multi;
// start a separate multi command queue
multi = client.multi();
multi.incr("incr thing", redis.print);
multi.incr("incr other thing", redis.print);
// runs immediately
client.mset("incr thing", 100, "incr other thing", 1, redis.print);
// drains multi queue and runs atomically
multi.exec(function (err, replies) {
console.log(replies); // 101, 2
});
// you can re-run the same transaction if you like
multi.exec(function (err, replies) {
console.log(replies); // 102, 3
client.quit();
});
In addition to adding commands to the `MULTI` queue individually, you can also pass an array
of commands and arguments to the constructor:
var redis = require("redis"),
client = redis.createClient(), multi;
client.multi([
["mget", "multifoo", "multibar", redis.print],
["incr", "multifoo"],
["incr", "multibar"]
]).exec(function (err, replies) {
console.log(replies);
});
## Monitor mode
Redis supports the `MONITOR` command, which lets you see all commands received by the Redis server
across all client connections, including from other client libraries and other computers.
After you send the `MONITOR` command, no other commands are valid on that connection. `node_redis`
will emit a `monitor` event for every new monitor message that comes across. The callback for the
`monitor` event takes a timestamp from the Redis server and an array of command arguments.
Here is a simple example:
var client = require("redis").createClient(),
util = require("util");
client.monitor(function (err, res) {
console.log("Entering monitoring mode.");
});
client.on("monitor", function (time, args) {
console.log(time + ": " + util.inspect(args));
});
# Extras
Some other things you might like to know about.
## client.server_info
After the ready probe completes, the results from the INFO command are saved in the `client.server_info`
object.
The `versions` key contains an array of the elements of the version string for easy comparison.
> client.server_info.redis_version
'2.3.0'
> client.server_info.versions
[ 2, 3, 0 ]
## redis.print()
A handy callback function for displaying return values when testing. Example:
var redis = require("redis"),
client = redis.createClient();
client.on("connect", function () {
client.set("foo_rand000000000000", "some fantastic value", redis.print);
client.get("foo_rand000000000000", redis.print);
});
This will print:
Reply: OK
Reply: some fantastic value
Note that this program will not exit cleanly because the client is still connected.
## redis.debug_mode
Boolean to enable debug mode and protocol tracing.
var redis = require("redis"),
client = redis.createClient();
redis.debug_mode = true;
client.on("connect", function () {
client.set("foo_rand000000000000", "some fantastic value");
});
This will display:
mjr:~/work/node_redis (master)$ node ~/example.js
send command: *3
$3
SET
$20
foo_rand000000000000
$20
some fantastic value
on_data: +OK
`send command` is data sent into Redis and `on_data` is data received from Redis.
## client.send_command(command_name, args, callback)
Used internally to send commands to Redis. For convenience, nearly all commands that are published on the Redis
Wiki have been added to the `client` object. However, if I missed any, or if new commands are introduced before
this library is updated, you can use `send_command()` to send arbitrary commands to Redis.
All commands are sent as multi-bulk commands. `args` can either be an Array of arguments, or individual arguments,
or omitted completely.
## client.connected
Boolean tracking the state of the connection to the Redis server.
## client.command_queue.length
The number of commands that have been sent to the Redis server but not yet replied to. You can use this to
enforce some kind of maximum queue depth for commands while connected.
Don't mess with `client.command_queue` though unless you really know what you are doing.
## client.offline_queue.length
The number of commands that have been queued up for a future connection. You can use this to enforce
some kind of maximum queue depth for pre-connection commands.
## client.retry_delay
Current delay in milliseconds before a connection retry will be attempted. This starts at `250`.
## client.retry_backoff
Multiplier for future retry timeouts. This should be larger than 1 to add more time between retries.
Defaults to 1.7. The default initial connection retry is 250, so the second retry will be 425, followed by 723.5, etc.
## TODO
Better tests for monitor mode, auth, disconnect/reconnect, and all combinations thereof.
Stream large set/get values into and out of Redis. Otherwise the entire value must be in node's memory.
Performance can be better for very large values.
I think there are more performance improvements left in there for smaller values, especially for large lists of small values.
## Contributors
Some people have have added features and fixed bugs in `node_redis` other than me.
In order of first contribution, they are:
* [Tim Smart](https://github.com/Tim-Smart)
* [TJ Holowaychuk](https://github.com/visionmedia)
* [Rick Olson](https://github.com/technoweenie)
* [Orion Henry](https://github.com/orionz)
* [Hank Sims](https://github.com/hanksims)
* [Aivo Paas](https://github.com/aivopaas)
* [Paul Carey](https://github.com/paulcarey)
* [Pieter Noordhuis](https://github.com/pietern)
* [Vladimir Dronnikov](https://github.com/dvv)
* [Dave Hoover](https://github.com/redsquirrel)
Thanks.
## LICENSE - "MIT License"
Copyright (c) 2010 Matthew Ranney, http://ranney.com/
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.
![spacer](http://ranney.com/1px.gif)

View file

@ -0,0 +1,184 @@
Changelog
=========
## v0.6.7 - July 30, 2011
(accidentally skipped v0.6.6)
Fix and test for [GH-123]
Passing an Array as as the last argument should expand as users
expect. The old behavior was to coerce the arguments into Strings,
which did surprising things with Arrays.
## v0.6.5 - July 6, 2011
Contributed changes:
* Support SlowBuffers (Umair Siddique)
* Add Multi to exports (Louis-Philippe Perron)
* Fix for drain event calculation (Vladimir Dronnikov)
Thanks!
## v0.6.4 - June 30, 2011
Fix bug with optional callbacks for hmset.
## v0.6.2 - June 30, 2011
Bugs fixed:
* authentication retry while server is loading db (danmaz74) [GH-101]
* command arguments processing issue with arrays
New features:
* Auto update of new commands from redis.io (Dave Hoover)
* Performance improvements and backpressure controls.
* Commands now return the true/false value from the underlying socket write(s).
* Implement command_queue high water and low water for more better control of queueing.
See `examples/backpressure_drain.js` for more information.
## v0.6.1 - June 29, 2011
Add support and tests for Redis scripting through EXEC command.
Bug fix for monitor mode. (forddg)
Auto update of new commands from redis.io (Dave Hoover)
## v0.6.0 - April 21, 2011
Lots of bugs fixed.
* connection error did not properly trigger reconnection logic [GH-85]
* client.hmget(key, [val1, val2]) was not expanding properly [GH-66]
* client.quit() while in pub/sub mode would throw an error [GH-87]
* client.multi(['hmset', 'key', {foo: 'bar'}]) fails [GH-92]
* unsubscribe before subscribe would make things very confused [GH-88]
* Add BRPOPLPUSH [GH-79]
## v0.5.11 - April 7, 2011
Added DISCARD
I originally didn't think DISCARD would do anything here because of the clever MULTI interface, but somebody
pointed out to me that DISCARD can be used to flush the WATCH set.
## v0.5.10 - April 6, 2011
Added HVALS
## v0.5.9 - March 14, 2011
Fix bug with empty Array arguments - Andy Ray
## v0.5.8 - March 14, 2011
Add `MONITOR` command and special monitor command reply parsing.
## v0.5.7 - February 27, 2011
Add magical auth command.
Authentication is now remembered by the client and will be automatically sent to the server
on every connection, including any reconnections.
## v0.5.6 - February 22, 2011
Fix bug in ready check with `return_buffers` set to `true`.
Thanks to Dean Mao and Austin Chau.
## v0.5.5 - February 16, 2011
Add probe for server readiness.
When a Redis server starts up, it might take a while to load the dataset into memory.
During this time, the server will accept connections, but will return errors for all non-INFO
commands. Now node_redis will send an INFO command whenever it connects to a server.
If the info command indicates that the server is not ready, the client will keep trying until
the server is ready. Once it is ready, the client will emit a "ready" event as well as the
"connect" event. The client will queue up all commands sent before the server is ready, just
like it did before. When the server is ready, all offline/non-ready commands will be replayed.
This should be backward compatible with previous versions.
To disable this ready check behavior, set `options.no_ready_check` when creating the client.
As a side effect of this change, the key/val params from the info command are available as
`client.server_options`. Further, the version string is decomposed into individual elements
in `client.server_options.versions`.
## v0.5.4 - February 11, 2011
Fix excess memory consumption from Queue backing store.
Thanks to Gustaf Sjöberg.
## v0.5.3 - February 5, 2011
Fix multi/exec error reply callback logic.
Thanks to Stella Laurenzo.
## v0.5.2 - January 18, 2011
Fix bug where unhandled error replies confuse the parser.
## v0.5.1 - January 18, 2011
Fix bug where subscribe commands would not handle redis-server startup error properly.
## v0.5.0 - December 29, 2010
Some bug fixes:
* An important bug fix in reconnection logic. Previously, reply callbacks would be invoked twice after
a reconnect.
* Changed error callback argument to be an actual Error object.
New feature:
* Add friendly syntax for HMSET using an object.
## v0.4.1 - December 8, 2010
Remove warning about missing hiredis. You probably do want it though.
## v0.4.0 - December 5, 2010
Support for multiple response parsers and hiredis C library from Pieter Noordhuis.
Return Strings instead of Buffers by default.
Empty nested mb reply bug fix.
## v0.3.9 - November 30, 2010
Fix parser bug on failed EXECs.
## v0.3.8 - November 10, 2010
Fix for null MULTI response when WATCH condition fails.
## v0.3.7 - November 9, 2010
Add "drain" and "idle" events.
## v0.3.6 - November 3, 2010
Add all known Redis commands from Redis master, even ones that are coming in 2.2 and beyond.
Send a friendlier "error" event message on stream errors like connection refused / reset.
## v0.3.5 - October 21, 2010
A few bug fixes.
* Fixed bug with `nil` multi-bulk reply lengths that showed up with `BLPOP` timeouts.
* Only emit `end` once when connection goes away.
* Fixed bug in `test.js` where driver finished before all tests completed.
## unversioned wasteland
See the git history for what happened before.

View file

@ -0,0 +1,9 @@
var redis = require("./index"),
client = redis.createClient();
redis.debug_mode = true;
client.eval("return 100.5", 0, function (err, res) {
console.dir(err);
console.dir(res);
});

View file

@ -0,0 +1,5 @@
var redis = require("redis"),
client = redis.createClient();
// This command is magical. Client stashes the password and will issue on every connect.
client.auth("somepass");

View file

@ -0,0 +1,33 @@
var redis = require("../index"),
client = redis.createClient(null, null, {
command_queue_high_water: 5,
command_queue_low_water: 1
}),
remaining_ops = 100000, paused = false;
function op() {
if (remaining_ops <= 0) {
console.error("Finished.");
process.exit(0);
}
remaining_ops--;
if (client.hset("test hash", "val " + remaining_ops, remaining_ops) === false) {
console.log("Pausing at " + remaining_ops);
paused = true;
} else {
process.nextTick(op);
}
}
client.on("drain", function () {
if (paused) {
console.log("Resuming at " + remaining_ops);
paused = false;
process.nextTick(op);
} else {
console.log("Got drain while not paused at " + remaining_ops);
}
});
op();

View file

@ -0,0 +1,24 @@
var redis = require("redis"),
client = redis.createClient();
// Extend the RedisClient prototype to add a custom method
// This one converts the results from "INFO" into a JavaScript Object
redis.RedisClient.prototype.parse_info = function (callback) {
this.info(function (err, res) {
var lines = res.toString().split("\r\n").sort();
var obj = {};
lines.forEach(function (line) {
var parts = line.split(':');
if (parts[1]) {
obj[parts[0]] = parts[1];
}
});
callback(obj)
});
};
client.parse_info(function (info) {
console.dir(info);
client.quit();
});

View file

@ -0,0 +1,32 @@
// Read a file from disk, store it in Redis, then read it back from Redis.
var redis = require("redis"),
client = redis.createClient(),
fs = require("fs"),
filename = "kids_in_cart.jpg";
// Get the file I use for testing like this:
// curl http://ranney.com/kids_in_cart.jpg -o kids_in_cart.jpg
// or just use your own file.
// Read a file from fs, store it in Redis, get it back from Redis, write it back to fs.
fs.readFile(filename, function (err, data) {
if (err) throw err
console.log("Read " + data.length + " bytes from filesystem.");
client.set(filename, data, redis.print); // set entire file
client.get(filename, function (err, reply) { // get entire file
if (err) {
console.log("Get error: " + err);
} else {
fs.writeFile("duplicate_" + filename, reply, function (err) {
if (err) {
console.log("Error on write: " + err)
} else {
console.log("File written.");
}
client.end();
});
}
});
});

View file

@ -0,0 +1,5 @@
var client = require("redis").createClient();
client.mget(["sessions started", "sessions started", "foo"], function (err, res) {
console.dir(res);
});

View file

@ -0,0 +1,10 @@
var client = require("../index").createClient(),
util = require("util");
client.monitor(function (err, res) {
console.log("Entering monitoring mode.");
});
client.on("monitor", function (time, args) {
console.log(time + ": " + util.inspect(args));
});

View file

@ -0,0 +1,46 @@
var redis = require("redis"),
client = redis.createClient(), set_size = 20;
client.sadd("bigset", "a member");
client.sadd("bigset", "another member");
while (set_size > 0) {
client.sadd("bigset", "member " + set_size);
set_size -= 1;
}
// multi chain with an individual callback
client.multi()
.scard("bigset")
.smembers("bigset")
.keys("*", function (err, replies) {
client.mget(replies, redis.print);
})
.dbsize()
.exec(function (err, replies) {
console.log("MULTI got " + replies.length + " replies");
replies.forEach(function (reply, index) {
console.log("Reply " + index + ": " + reply.toString());
});
});
client.mset("incr thing", 100, "incr other thing", 1, redis.print);
// start a separate multi command queue
var multi = client.multi();
multi.incr("incr thing", redis.print);
multi.incr("incr other thing", redis.print);
// runs immediately
client.get("incr thing", redis.print); // 100
// drains multi queue and runs atomically
multi.exec(function (err, replies) {
console.log(replies); // 101, 2
});
// you can re-run the same transaction if you like
multi.exec(function (err, replies) {
console.log(replies); // 102, 3
client.quit();
});

View file

@ -0,0 +1,29 @@
var redis = require("redis"),
client = redis.createClient(), multi;
// start a separate command queue for multi
multi = client.multi();
multi.incr("incr thing", redis.print);
multi.incr("incr other thing", redis.print);
// runs immediately
client.mset("incr thing", 100, "incr other thing", 1, redis.print);
// drains multi queue and runs atomically
multi.exec(function (err, replies) {
console.log(replies); // 101, 2
});
// you can re-run the same transaction if you like
multi.exec(function (err, replies) {
console.log(replies); // 102, 3
client.quit();
});
client.multi([
["mget", "multifoo", "multibar", redis.print],
["incr", "multifoo"],
["incr", "multibar"]
]).exec(function (err, replies) {
console.log(replies.toString());
});

View file

@ -0,0 +1,33 @@
var redis = require("redis"),
client1 = redis.createClient(),
client2 = redis.createClient(),
client3 = redis.createClient(),
client4 = redis.createClient(),
msg_count = 0;
redis.debug_mode = false;
client1.on("psubscribe", function (pattern, count) {
console.log("client1 psubscribed to " + pattern + ", " + count + " total subscriptions");
client2.publish("channeltwo", "Me!");
client3.publish("channelthree", "Me too!");
client4.publish("channelfour", "And me too!");
});
client1.on("punsubscribe", function (pattern, count) {
console.log("client1 punsubscribed from " + pattern + ", " + count + " total subscriptions");
client4.end();
client3.end();
client2.end();
client1.end();
});
client1.on("pmessage", function (pattern, channel, message) {
console.log("("+ pattern +")" + " client1 received message on " + channel + ": " + message);
msg_count += 1;
if (msg_count === 3) {
client1.punsubscribe();
}
});
client1.psubscribe("channel*");

View file

@ -0,0 +1,41 @@
var redis = require("redis"),
client1 = redis.createClient(), msg_count = 0,
client2 = redis.createClient();
redis.debug_mode = false;
// Most clients probably don't do much on "subscribe". This example uses it to coordinate things within one program.
client1.on("subscribe", function (channel, count) {
console.log("client1 subscribed to " + channel + ", " + count + " total subscriptions");
if (count === 2) {
client2.publish("a nice channel", "I am sending a message.");
client2.publish("another one", "I am sending a second message.");
client2.publish("a nice channel", "I am sending my last message.");
}
});
client1.on("unsubscribe", function (channel, count) {
console.log("client1 unsubscribed from " + channel + ", " + count + " total subscriptions");
if (count === 0) {
client2.end();
client1.end();
}
});
client1.on("message", function (channel, message) {
console.log("client1 channel " + channel + ": " + message);
msg_count += 1;
if (msg_count === 3) {
client1.unsubscribe();
}
});
client1.on("ready", function () {
// if you need auth, do it here
client1.incr("did a thing");
client1.subscribe("a nice channel", "another one");
});
client2.on("ready", function () {
// if you need auth, do it here
});

View file

@ -0,0 +1,17 @@
var redis = require("redis"),
client = redis.createClient();
client.on("error", function (err) {
console.log("Redis connection error to " + client.host + ":" + client.port + " - " + err);
});
client.set("string key", "string val", redis.print);
client.hset("hash key", "hashtest 1", "some value", redis.print);
client.hset(["hash key", "hashtest 2", "some other value"], redis.print);
client.hkeys("hash key", function (err, replies) {
console.log(replies.length + " replies:");
replies.forEach(function (reply, i) {
console.log(" " + i + ": " + reply);
});
client.quit();
});

View file

@ -0,0 +1,15 @@
// Sending commands in response to other commands.
// This example runs "type" against every key in the database
//
var client = require("redis").createClient();
client.keys("*", function (err, keys) {
keys.forEach(function (key, pos) {
client.type(key, function (err, keytype) {
console.log(key + " is " + keytype);
if (pos === (keys.length - 1)) {
client.quit();
}
});
});
});

View file

@ -0,0 +1,19 @@
var client = require("redis").createClient();
function print_results(obj) {
console.dir(obj);
}
// build a map of all keys and their types
client.keys("*", function (err, all_keys) {
var key_types = {};
all_keys.forEach(function (key, pos) { // use second arg of forEach to get pos
client.type(key, function (err, type) {
key_types[key] = type;
if (pos === all_keys.length - 1) { // callbacks all run in order
print_results(key_types);
}
});
});
});

View file

@ -0,0 +1,29 @@
var redis = require("redis"),
client = redis.createClient("/tmp/redis.sock"),
profiler = require("v8-profiler");
client.on("connect", function () {
console.log("Got Unix socket connection.")
});
client.on("error", function (err) {
console.log(err.message);
});
client.set("space chars", "space value");
setInterval(function () {
client.get("space chars");
}, 100);
function done() {
client.info(function (err, reply) {
console.log(reply.toString());
client.quit();
});
}
setTimeout(function () {
console.log("Taking snapshot.");
var snap = profiler.takeSnapshot();
}, 5000);

View file

@ -0,0 +1,31 @@
// A simple web server that generates dyanmic content based on responses from Redis
var http = require("http"), server,
redis_client = require("redis").createClient();
server = http.createServer(function (request, response) {
response.writeHead(200, {
"Content-Type": "text/plain"
});
var redis_info, total_requests;
redis_client.info(function (err, reply) {
redis_info = reply; // stash response in outer scope
});
redis_client.incr("requests", function (err, reply) {
total_requests = reply; // stash response in outer scope
});
redis_client.hincrby("ip", request.connection.remoteAddress, 1);
redis_client.hgetall("ip", function (err, reply) {
// This is the last reply, so all of the previous replies must have completed already
response.write("This page was generated after talking to redis.\n\n" +
"Redis info:\n" + redis_info + "\n" +
"Total requests: " + total_requests + "\n\n" +
"IP count: \n");
Object.keys(reply).forEach(function (ip) {
response.write(" " + ip + ": " + reply[ip] + "\n");
});
response.end();
});
}).listen(80);

View file

@ -0,0 +1,40 @@
var http = require("http"),
sys = require("sys"),
fs = require("fs");
function prettyCurrentTime() {
var date = new Date();
return date.toLocaleString();
}
function write_file(commands, path) {
var file_contents, out_commands;
console.log("Writing " + Object.keys(commands).length + " commands to " + path);
file_contents = "// This file was generated by ./generate_commands.js on " + prettyCurrentTime() + "\n";
out_commands = Object.keys(commands).map(function (key) {
return key.toLowerCase();
});
file_contents += "module.exports = " + JSON.stringify(out_commands, null, " ") + ";\n";
fs.writeFile(path, file_contents);
}
http.get({host: "redis.io", path: "/commands.json"}, function (res) {
var body = "";
console.log("Response from redis.io/commands.json: " + res.statusCode);
res.on('data', function (chunk) {
body += chunk;
});
res.on('end', function () {
write_file(JSON.parse(body), "lib/commands.js");
});
}).on('error', function (e) {
console.log("Error fetching command list from redis.io: " + e.message);
});

View file

@ -0,0 +1,860 @@
/*global Buffer require exports console setTimeout */
var net = require("net"),
util = require("./lib/util").util,
Queue = require("./lib/queue").Queue,
to_array = require("./lib/to_array"),
events = require("events"),
parsers = [], commands,
default_port = 6379,
default_host = "127.0.0.1";
// can set this to true to enable for all connections
exports.debug_mode = false;
// hiredis might not be installed
try {
require("./lib/parser/hiredis");
parsers.push(require("./lib/parser/hiredis"));
} catch (err) {
if (exports.debug_mode) {
console.log("hiredis parser not installed.");
}
}
parsers.push(require("./lib/parser/javascript"));
function RedisClient(stream, options) {
this.stream = stream;
this.options = options || {};
this.connected = false;
this.ready = false;
this.connections = 0;
this.attempts = 1;
this.should_buffer = false;
this.command_queue_high_water = this.options.command_queue_high_water || 1000;
this.command_queue_low_water = this.options.command_queue_low_water || 0;
this.command_queue = new Queue(); // holds sent commands to de-pipeline them
this.offline_queue = new Queue(); // holds commands issued but not able to be sent
this.commands_sent = 0;
this.retry_delay = 250; // inital reconnection delay
this.current_retry_delay = this.retry_delay;
this.retry_backoff = 1.7; // each retry waits current delay * retry_backoff
this.subscriptions = false;
this.monitoring = false;
this.closing = false;
this.server_info = {};
this.auth_pass = null;
var parser_module, self = this;
if (self.options.parser) {
if (! parsers.some(function (parser) {
if (parser.name === self.options.parser) {
parser_module = parser;
if (exports.debug_mode) {
console.log("Using parser module: " + parser_module.name);
}
return true;
}
})) {
throw new Error("Couldn't find named parser " + self.options.parser + " on this system");
}
} else {
if (exports.debug_mode) {
console.log("Using default parser module: " + parsers[0].name);
}
parser_module = parsers[0];
}
parser_module.debug_mode = exports.debug_mode;
this.reply_parser = new parser_module.Parser({
return_buffers: self.options.return_buffers || false
});
// "reply error" is an error sent back by Redis
this.reply_parser.on("reply error", function (reply) {
self.return_error(new Error(reply));
});
this.reply_parser.on("reply", function (reply) {
self.return_reply(reply);
});
// "error" is bad. Somehow the parser got confused. It'll try to reset and continue.
this.reply_parser.on("error", function (err) {
self.emit("error", new Error("Redis reply parser error: " + err.stack));
});
this.stream.on("connect", function () {
self.on_connect();
});
this.stream.on("data", function (buffer_from_socket) {
self.on_data(buffer_from_socket);
});
this.stream.on("error", function (msg) {
if (this.closing) {
return;
}
var message = "Redis connection to " + self.host + ":" + self.port + " failed - " + msg.message;
if (exports.debug_mode) {
console.warn(message);
}
self.offline_queue.forEach(function (args) {
if (typeof args[2] === "function") {
args[2](message);
}
});
self.offline_queue = new Queue();
self.command_queue.forEach(function (args) {
if (typeof args[2] === "function") {
args[2](message);
}
});
self.command_queue = new Queue();
self.connected = false;
self.ready = false;
self.emit("error", new Error(message));
// "error" events get turned into exceptions if they aren't listened for. If the user handled this error
// then we should try to reconnect.
self.connection_gone("error");
});
this.stream.on("close", function () {
self.connection_gone("close");
});
this.stream.on("end", function () {
self.connection_gone("end");
});
this.stream.on("drain", function () {
self.should_buffer = false;
self.emit("drain");
});
events.EventEmitter.call(this);
}
util.inherits(RedisClient, events.EventEmitter);
exports.RedisClient = RedisClient;
RedisClient.prototype.do_auth = function () {
var self = this;
if (exports.debug_mode) {
console.log("Sending auth to " + self.host + ":" + self.port + " fd " + self.stream.fd);
}
self.send_anyway = true;
self.send_command("auth", [this.auth_pass], function (err, res) {
if (err) {
if (err.toString().match("LOADING")) {
// if redis is still loading the db, it will not authenticate and everything else will fail
console.log("Redis still loading, trying to authenticate later");
setTimeout(function () {
self.do_auth();
}, 2000); // TODO - magic number alert
return;
} else {
return self.emit("error", "Auth error: " + err);
}
}
if (res.toString() !== "OK") {
return self.emit("error", "Auth failed: " + res.toString());
}
if (exports.debug_mode) {
console.log("Auth succeeded " + self.host + ":" + self.port + " fd " + self.stream.fd);
}
if (self.auth_callback) {
self.auth_callback(err, res);
self.auth_callback = null;
}
// now we are really connected
self.emit("connect");
if (self.options.no_ready_check) {
self.ready = true;
self.send_offline_queue();
} else {
self.ready_check();
}
});
self.send_anyway = false;
};
RedisClient.prototype.on_connect = function () {
if (exports.debug_mode) {
console.log("Stream connected " + this.host + ":" + this.port + " fd " + this.stream.fd);
}
var self = this;
this.connected = true;
this.ready = false;
this.attempts = 0;
this.connections += 1;
this.command_queue = new Queue();
this.emitted_end = false;
this.retry_timer = null;
this.current_retry_delay = this.retry_time;
this.stream.setNoDelay();
this.stream.setTimeout(0);
if (this.auth_pass) {
this.do_auth();
} else {
this.emit("connect");
if (this.options.no_ready_check) {
this.ready = true;
this.send_offline_queue();
} else {
this.ready_check();
}
}
};
RedisClient.prototype.ready_check = function () {
var self = this;
function send_info_cmd() {
if (exports.debug_mode) {
console.log("checking server ready state...");
}
self.send_anyway = true; // secret flag to send_command to send something even if not "ready"
self.info(function (err, res) {
if (err) {
return self.emit("error", "Ready check failed: " + err);
}
var lines = res.toString().split("\r\n"), obj = {}, retry_time;
lines.forEach(function (line) {
var parts = line.split(':');
if (parts[1]) {
obj[parts[0]] = parts[1];
}
});
obj.versions = [];
obj.redis_version.split('.').forEach(function (num) {
obj.versions.push(+num);
});
// expose info key/vals to users
self.server_info = obj;
if (!obj.loading || (obj.loading && obj.loading === "0")) {
if (exports.debug_mode) {
console.log("Redis server ready.");
}
self.ready = true;
self.send_offline_queue();
self.emit("ready");
} else {
retry_time = obj.loading_eta_seconds * 1000;
if (retry_time > 1000) {
retry_time = 1000;
}
if (exports.debug_mode) {
console.log("Redis server still loading, trying again in " + retry_time);
}
setTimeout(send_info_cmd, retry_time);
}
});
self.send_anyway = false;
}
send_info_cmd();
};
RedisClient.prototype.send_offline_queue = function () {
var command_obj, buffered_writes = 0;
while (this.offline_queue.length > 0) {
command_obj = this.offline_queue.shift();
if (exports.debug_mode) {
console.log("Sending offline command: " + command_obj.command);
}
buffered_writes += !this.send_command(command_obj.command, command_obj.args, command_obj.callback);
}
this.offline_queue = new Queue();
// Even though items were shifted off, Queue backing store still uses memory until next add, so just get a new Queue
if (!buffered_writes) {
this.should_buffer = false;
this.emit("drain");
}
};
RedisClient.prototype.connection_gone = function (why) {
var self = this;
// If a retry is already in progress, just let that happen
if (this.retry_timer) {
return;
}
// Note that this may trigger another "close" or "end" event
this.stream.destroy();
if (exports.debug_mode) {
console.warn("Redis connection is gone from " + why + " event.");
}
this.connected = false;
this.ready = false;
this.subscriptions = false;
this.monitoring = false;
// since we are collapsing end and close, users don't expect to be called twice
if (! this.emitted_end) {
this.emit("end");
this.emitted_end = true;
}
this.command_queue.forEach(function (args) {
if (typeof args[2] === "function") {
args[2]("Server connection closed");
}
});
this.command_queue = new Queue();
// If this is a requested shutdown, then don't retry
if (this.closing) {
this.retry_timer = null;
return;
}
this.current_retry_delay = this.retry_delay * this.retry_backoff;
if (exports.debug_mode) {
console.log("Retry connection in " + this.current_retry_delay + " ms");
}
this.attempts += 1;
this.emit("reconnecting", {
delay: this.current_retry_delay,
attempt: this.attempts
});
this.retry_timer = setTimeout(function () {
if (exports.debug_mode) {
console.log("Retrying connection...");
}
self.stream.connect(self.port, self.host);
self.retry_timer = null;
}, this.current_retry_delay);
};
RedisClient.prototype.on_data = function (data) {
if (exports.debug_mode) {
console.log("net read " + this.host + ":" + this.port + " fd " + this.stream.fd + ": " + data.toString());
}
try {
this.reply_parser.execute(data);
} catch (err) {
// This is an unexpected parser problem, an exception that came from the parser code itself.
// Parser should emit "error" events if it notices things are out of whack.
// Callbacks that throw exceptions will land in return_reply(), below.
// TODO - it might be nice to have a different "error" event for different types of errors
this.emit("error", err);
}
};
RedisClient.prototype.return_error = function (err) {
var command_obj = this.command_queue.shift(), queue_len = this.command_queue.getLength();
if (this.subscriptions === false && queue_len === 0) {
this.emit("idle");
this.command_queue = new Queue();
}
if (this.should_buffer && queue_len <= this.command_queue_low_water) {
this.emit("drain");
this.should_buffer = false;
}
if (command_obj && typeof command_obj.callback === "function") {
try {
command_obj.callback(err);
} catch (callback_err) {
// if a callback throws an exception, re-throw it on a new stack so the parser can keep going
process.nextTick(function () {
throw callback_err;
});
}
} else {
console.log("node_redis: no callback to send error: " + err.message);
// this will probably not make it anywhere useful, but we might as well throw
process.nextTick(function () {
throw err;
});
}
};
RedisClient.prototype.return_reply = function (reply) {
var command_obj = this.command_queue.shift(),
obj, i, len, key, val, type, timestamp, args, queue_len = this.command_queue.getLength();
if (this.subscriptions === false && queue_len === 0) {
this.emit("idle");
this.command_queue = new Queue(); // explicitly reclaim storage from old Queue
}
if (this.should_buffer && queue_len <= this.command_queue_low_water) {
this.emit("drain");
this.should_buffer = false;
}
if (command_obj && !command_obj.sub_command) {
if (typeof command_obj.callback === "function") {
// HGETALL special case replies with keyed Buffers
if (reply && 'hgetall' === command_obj.command.toLowerCase()) {
obj = {};
for (i = 0, len = reply.length; i < len; i += 2) {
key = reply[i].toString();
val = reply[i + 1];
obj[key] = val;
}
reply = obj;
}
try {
command_obj.callback(null, reply);
} catch (err) {
// if a callback throws an exception, re-throw it on a new stack so the parser can keep going
process.nextTick(function () {
throw err;
});
}
} else if (exports.debug_mode) {
console.log("no callback for reply: " + (reply && reply.toString && reply.toString()));
}
} else if (this.subscriptions || (command_obj && command_obj.sub_command)) {
if (Array.isArray(reply)) {
type = reply[0].toString();
if (type === "message") {
this.emit("message", reply[1].toString(), reply[2]); // channel, message
} else if (type === "pmessage") {
this.emit("pmessage", reply[1].toString(), reply[2].toString(), reply[3]); // pattern, channel, message
} else if (type === "subscribe" || type === "unsubscribe" || type === "psubscribe" || type === "punsubscribe") {
if (reply[2] === 0) {
this.subscriptions = false;
if (this.debug_mode) {
console.log("All subscriptions removed, exiting pub/sub mode");
}
}
this.emit(type, reply[1].toString(), reply[2]); // channel, count
} else {
throw new Error("subscriptions are active but got unknown reply type " + type);
}
} else if (! this.closing) {
throw new Error("subscriptions are active but got an invalid reply: " + reply);
}
} else if (this.monitoring) {
len = reply.indexOf(" ");
timestamp = reply.slice(0, len);
// TODO - this de-quoting doesn't work correctly if you put JSON strings in your values.
args = reply.slice(len + 1).match(/"[^"]+"/g).map(function (elem) {
return elem.replace(/"/g, "");
});
this.emit("monitor", timestamp, args);
} else {
throw new Error("node_redis command queue state error. If you can reproduce this, please report it.");
}
};
// This Command constructor is ever so slightly faster than using an object literal
function Command(command, args, sub_command, callback) {
this.command = command;
this.args = args;
this.sub_command = sub_command;
this.callback = callback;
}
RedisClient.prototype.send_command = function (command, args, callback) {
var arg, this_args, command_obj, i, il, elem_count, stream = this.stream, buffer_args, command_str = "", buffered_writes = 0;
if (typeof command !== "string") {
throw new Error("First argument to send_command must be the command name string, not " + typeof command);
}
if (Array.isArray(args)) {
if (typeof callback === "function") {
// probably the fastest way:
// client.command([arg1, arg2], cb); (straight passthrough)
// send_command(command, [arg1, arg2], cb);
} else if (! callback) {
// most people find this variable argument length form more convenient, but it uses arguments, which is slower
// client.command(arg1, arg2, cb); (wraps up arguments into an array)
// send_command(command, [arg1, arg2, cb]);
// client.command(arg1, arg2); (callback is optional)
// send_command(command, [arg1, arg2]);
if (typeof args[args.length - 1] === "function") {
callback = args[args.length - 1];
args.length -= 1;
}
} else {
throw new Error("send_command: last argument must be a callback or undefined");
}
} else {
throw new Error("send_command: second argument must be an array");
}
// if the last argument is an array, expand it out. This allows commands like this:
// client.command(arg1, [arg2, arg3, arg4], cb);
// and converts to:
// client.command(arg1, arg2, arg3, arg4, cb);
// which is convenient for some things like sadd
if (Array.isArray(args[args.length - 1])) {
args = args.slice(0, -1).concat(args[args.length - 1]);
}
command_obj = new Command(command, args, false, callback);
if ((!this.ready && !this.send_anyway) || !stream.writable) {
if (exports.debug_mode) {
if (!stream.writable) {
console.log("send command: stream is not writeable.");
}
console.log("Queueing " + command + " for next server connection.");
}
this.offline_queue.push(command_obj);
this.should_buffer = true;
return false;
}
if (command === "subscribe" || command === "psubscribe" || command === "unsubscribe" || command === "punsubscribe") {
if (this.subscriptions === false && exports.debug_mode) {
console.log("Entering pub/sub mode from " + command);
}
command_obj.sub_command = true;
this.subscriptions = true;
} else if (command === "monitor") {
this.monitoring = true;
} else if (command === "quit") {
this.closing = true;
} else if (this.subscriptions === true) {
throw new Error("Connection in pub/sub mode, only pub/sub commands may be used");
}
this.command_queue.push(command_obj);
this.commands_sent += 1;
elem_count = 1;
buffer_args = false;
elem_count += args.length;
// Always use "Multi bulk commands", but if passed any Buffer args, then do multiple writes, one for each arg
// This means that using Buffers in commands is going to be slower, so use Strings if you don't already have a Buffer.
// Also, why am I putting user documentation in the library source code?
command_str = "*" + elem_count + "\r\n$" + command.length + "\r\n" + command + "\r\n";
for (i = 0, il = args.length, arg; i < il; i += 1) {
if (Buffer.isBuffer(args[i])) {
buffer_args = true;
}
}
if (! buffer_args) { // Build up a string and send entire command in one write
for (i = 0, il = args.length, arg; i < il; i += 1) {
arg = args[i];
if (typeof arg !== "string") {
arg = String(arg);
}
command_str += "$" + Buffer.byteLength(arg) + "\r\n" + arg + "\r\n";
}
if (exports.debug_mode) {
console.log("send " + this.host + ":" + this.port + " fd " + this.stream.fd + ": " + command_str);
}
buffered_writes += !stream.write(command_str);
} else {
if (exports.debug_mode) {
console.log("send command (" + command_str + ") has Buffer arguments");
}
buffered_writes += !stream.write(command_str);
for (i = 0, il = args.length, arg; i < il; i += 1) {
arg = args[i];
if (!(Buffer.isBuffer(arg) || arg instanceof String)) {
arg = String(arg);
}
if (Buffer.isBuffer(arg)) {
if (arg.length === 0) {
if (exports.debug_mode) {
console.log("send_command: using empty string for 0 length buffer");
}
buffered_writes += !stream.write("$0\r\n\r\n");
} else {
buffered_writes += !stream.write("$" + arg.length + "\r\n");
buffered_writes += !stream.write(arg);
buffered_writes += !stream.write("\r\n");
if (exports.debug_mode) {
console.log("send_command: buffer send " + arg.length + " bytes");
}
}
} else {
if (exports.debug_mode) {
console.log("send_command: string send " + Buffer.byteLength(arg) + " bytes: " + arg);
}
buffered_writes += !stream.write("$" + Buffer.byteLength(arg) + "\r\n" + arg + "\r\n");
}
}
}
if (exports.debug_mode) {
console.log("send_command buffered_writes: " + buffered_writes, " should_buffer: " + this.should_buffer);
}
if (buffered_writes || this.command_queue.getLength() >= this.command_queue_high_water) {
this.should_buffer = true;
}
return !this.should_buffer;
};
RedisClient.prototype.end = function () {
this.stream._events = {};
this.connected = false;
this.ready = false;
return this.stream.end();
};
function Multi(client, args) {
this.client = client;
this.queue = [["MULTI"]];
if (Array.isArray(args)) {
this.queue = this.queue.concat(args);
}
}
exports.Multi = Multi;
// take 2 arrays and return the union of their elements
function set_union(seta, setb) {
var obj = {};
seta.forEach(function (val) {
obj[val] = true;
});
setb.forEach(function (val) {
obj[val] = true;
});
return Object.keys(obj);
}
// This static list of commands is updated from time to time. ./lib/commands.js can be updated with generate_commands.js
commands = set_union(["get", "set", "setnx", "setex", "append", "strlen", "del", "exists", "setbit", "getbit", "setrange", "getrange", "substr",
"incr", "decr", "mget", "rpush", "lpush", "rpushx", "lpushx", "linsert", "rpop", "lpop", "brpop", "brpoplpush", "blpop", "llen", "lindex",
"lset", "lrange", "ltrim", "lrem", "rpoplpush", "sadd", "srem", "smove", "sismember", "scard", "spop", "srandmember", "sinter", "sinterstore",
"sunion", "sunionstore", "sdiff", "sdiffstore", "smembers", "zadd", "zincrby", "zrem", "zremrangebyscore", "zremrangebyrank", "zunionstore",
"zinterstore", "zrange", "zrangebyscore", "zrevrangebyscore", "zcount", "zrevrange", "zcard", "zscore", "zrank", "zrevrank", "hset", "hsetnx",
"hget", "hmset", "hmget", "hincrby", "hdel", "hlen", "hkeys", "hvals", "hgetall", "hexists", "incrby", "decrby", "getset", "mset", "msetnx",
"randomkey", "select", "move", "rename", "renamenx", "expire", "expireat", "keys", "dbsize", "auth", "ping", "echo", "save", "bgsave",
"bgrewriteaof", "shutdown", "lastsave", "type", "multi", "exec", "discard", "sync", "flushdb", "flushall", "sort", "info", "monitor", "ttl",
"persist", "slaveof", "debug", "config", "subscribe", "unsubscribe", "psubscribe", "punsubscribe", "publish", "watch", "unwatch", "cluster",
"restore", "migrate", "dump", "object", "client", "eval", "evalsha"], require("./lib/commands"));
commands.forEach(function (command) {
RedisClient.prototype[command] = function (args, callback) {
if (Array.isArray(args) && typeof callback === "function") {
return this.send_command(command, args, callback);
} else {
return this.send_command(command, to_array(arguments));
}
};
RedisClient.prototype[command.toUpperCase()] = RedisClient.prototype[command];
Multi.prototype[command] = function () {
this.queue.push([command].concat(to_array(arguments)));
return this;
};
Multi.prototype[command.toUpperCase()] = Multi.prototype[command];
});
// Stash auth for connect and reconnect. Send immediately if already connected.
RedisClient.prototype.auth = function () {
var args = to_array(arguments);
this.auth_pass = args[0];
this.auth_callback = args[1];
if (exports.debug_mode) {
console.log("Saving auth as " + this.auth_pass);
}
if (this.connected) {
this.send_command("auth", args);
}
};
RedisClient.prototype.AUTH = RedisClient.prototype.auth;
RedisClient.prototype.hmget = function (arg1, arg2, arg3) {
if (Array.isArray(arg2) && typeof arg3 === "function") {
return this.send_command("hmget", [arg1].concat(arg2), arg3);
} else if (Array.isArray(arg1) && typeof arg2 === "function") {
return this.send_command("hmget", arg1, arg2);
} else {
return this.send_command("hmget", to_array(arguments));
}
};
RedisClient.prototype.HMGET = RedisClient.prototype.hmget;
RedisClient.prototype.hmset = function (args, callback) {
var tmp_args, tmp_keys, i, il, key;
if (Array.isArray(args) && typeof callback === "function") {
return this.send_command("hmset", args, callback);
}
args = to_array(arguments);
if (typeof args[args.length - 1] === "function") {
callback = args[args.length - 1];
args.length -= 1;
} else {
callback = null;
}
if (args.length === 2 && typeof args[0] === "string" && typeof args[1] === "object") {
// User does: client.hmset(key, {key1: val1, key2: val2})
tmp_args = [ args[0] ];
tmp_keys = Object.keys(args[1]);
for (i = 0, il = tmp_keys.length; i < il ; i++) {
key = tmp_keys[i];
tmp_args.push(key);
tmp_args.push(args[1][key]);
}
args = tmp_args;
}
return this.send_command("hmset", args, callback);
};
RedisClient.prototype.HMSET = RedisClient.prototype.hmset;
Multi.prototype.hmset = function () {
var args = to_array(arguments), tmp_args;
if (args.length >= 2 && typeof args[0] === "string" && typeof args[1] === "object") {
tmp_args = [ "hmset", args[0] ];
Object.keys(args[1]).map(function (key) {
tmp_args.push(key);
tmp_args.push(args[1][key]);
});
if (args[2]) {
tmp_args.push(args[2]);
}
args = tmp_args;
} else {
args.unshift("hmset");
}
this.queue.push(args);
return this;
};
Multi.prototype.HMSET = Multi.prototype.hmset;
Multi.prototype.exec = function (callback) {
var self = this;
// drain queue, callback will catch "QUEUED" or error
// TODO - get rid of all of these anonymous functions which are elegant but slow
this.queue.forEach(function (args, index) {
var command = args[0], obj;
if (typeof args[args.length - 1] === "function") {
args = args.slice(1, -1);
} else {
args = args.slice(1);
}
if (args.length === 1 && Array.isArray(args[0])) {
args = args[0];
}
if (command === 'hmset' && typeof args[1] === 'object') {
obj = args.pop();
Object.keys(obj).forEach(function (key) {
args.push(key);
args.push(obj[key]);
});
}
this.client.send_command(command, args, function (err, reply) {
if (err) {
var cur = self.queue[index];
if (typeof cur[cur.length - 1] === "function") {
cur[cur.length - 1](err);
} else {
throw new Error(err);
}
self.queue.splice(index, 1);
}
});
}, this);
// TODO - make this callback part of Multi.prototype instead of creating it each time
return this.client.send_command("EXEC", [], function (err, replies) {
if (err) {
if (callback) {
callback(new Error(err));
return;
} else {
throw new Error(err);
}
}
var i, il, j, jl, reply, args, obj, key, val;
if (replies) {
for (i = 1, il = self.queue.length; i < il; i += 1) {
reply = replies[i - 1];
args = self.queue[i];
// Convert HGETALL reply to object
if (reply && args[0].toLowerCase() === "hgetall") {
obj = {};
for (j = 0, jl = reply.length; j < jl; j += 2) {
key = reply[j].toString();
val = reply[j + 1];
obj[key] = val;
}
replies[i - 1] = reply = obj;
}
if (typeof args[args.length - 1] === "function") {
args[args.length - 1](null, reply);
}
}
}
if (callback) {
callback(null, replies);
}
});
};
RedisClient.prototype.multi = function (args) {
return new Multi(this, args);
};
RedisClient.prototype.MULTI = function (args) {
return new Multi(this, args);
};
exports.createClient = function (port_arg, host_arg, options) {
var port = port_arg || default_port,
host = host_arg || default_host,
redis_client, net_client;
net_client = net.createConnection(port, host);
redis_client = new RedisClient(net_client, options);
redis_client.port = port;
redis_client.host = host;
return redis_client;
};
exports.print = function (err, reply) {
if (err) {
console.log("Error: " + err);
} else {
console.log("Reply: " + reply);
}
};

View file

@ -0,0 +1,126 @@
// This file was generated by ./generate_commands.js on Tue Jun 28 2011 22:37:02 GMT-0700 (PDT)
module.exports = [
"append",
"auth",
"bgrewriteaof",
"bgsave",
"blpop",
"brpop",
"brpoplpush",
"config get",
"config set",
"config resetstat",
"dbsize",
"debug object",
"debug segfault",
"decr",
"decrby",
"del",
"discard",
"echo",
"exec",
"exists",
"expire",
"expireat",
"flushall",
"flushdb",
"get",
"getbit",
"getrange",
"getset",
"hdel",
"hexists",
"hget",
"hgetall",
"hincrby",
"hkeys",
"hlen",
"hmget",
"hmset",
"hset",
"hsetnx",
"hvals",
"incr",
"incrby",
"info",
"keys",
"lastsave",
"lindex",
"linsert",
"llen",
"lpop",
"lpush",
"lpushx",
"lrange",
"lrem",
"lset",
"ltrim",
"mget",
"monitor",
"move",
"mset",
"msetnx",
"multi",
"object",
"persist",
"ping",
"psubscribe",
"publish",
"punsubscribe",
"quit",
"randomkey",
"rename",
"renamenx",
"rpop",
"rpoplpush",
"rpush",
"rpushx",
"sadd",
"save",
"scard",
"sdiff",
"sdiffstore",
"select",
"set",
"setbit",
"setex",
"setnx",
"setrange",
"shutdown",
"sinter",
"sinterstore",
"sismember",
"slaveof",
"smembers",
"smove",
"sort",
"spop",
"srandmember",
"srem",
"strlen",
"subscribe",
"sunion",
"sunionstore",
"sync",
"ttl",
"type",
"unsubscribe",
"unwatch",
"watch",
"zadd",
"zcard",
"zcount",
"zincrby",
"zinterstore",
"zrange",
"zrangebyscore",
"zrank",
"zrem",
"zremrangebyrank",
"zremrangebyscore",
"zrevrange",
"zrevrangebyscore",
"zrevrank",
"zscore",
"zunionstore"
];

View file

@ -0,0 +1,41 @@
/*global Buffer require exports console setTimeout */
var events = require("events"),
util = require("../util").util,
hiredis = require("hiredis");
exports.debug_mode = false;
exports.name = "hiredis";
function HiredisReplyParser(options) {
this.name = exports.name;
this.options = options || {};
this.reset();
events.EventEmitter.call(this);
}
util.inherits(HiredisReplyParser, events.EventEmitter);
exports.Parser = HiredisReplyParser;
HiredisReplyParser.prototype.reset = function () {
this.reader = new hiredis.Reader({
return_buffers: this.options.return_buffers || false
});
};
HiredisReplyParser.prototype.execute = function (data) {
var reply;
this.reader.feed(data);
try {
while ((reply = this.reader.get()) !== undefined) {
if (reply && reply.constructor === Error) {
this.emit("reply error", reply);
} else {
this.emit("reply", reply);
}
}
} catch (err) {
this.emit("error", err);
}
};

Some files were not shown because too many files have changed in this diff Show more