Browse files

Adding Socket.IO

  • Loading branch information...
1 parent 26c3ab0 commit 8a9712a40e40551afa34e4fbec3951077c4a2885 Jonas Galvez committed Aug 29, 2010
View
9 lib/socket.io/.gitmodules
@@ -0,0 +1,9 @@
+[submodule "example/client"]
+ path = example/client
+ url = git://github.com/LearnBoost/Socket.IO.git
+[submodule "tests/support/expresso"]
+ path = tests/support/expresso
+ url = git://github.com/visionmedia/expresso.git
+[submodule "tests/support/node-websocket-client"]
+ path = tests/support/node-websocket-client
+ url = git://github.com/pgriess/node-websocket-client.git
View
2 lib/socket.io/Makefile
@@ -0,0 +1,2 @@
+test:
+ ./tests/support/expresso/bin/expresso -I lib $(TESTFLAGS) tests/*.test.js
View
216 lib/socket.io/README.md
@@ -0,0 +1,216 @@
+Socket.IO Server: Sockets for the rest of us
+============================================
+
+The `Socket.IO` server provides seamless supports for a variety of transports intended for realtime communication
+
+- WebSocket (with Flash policy support)
+- XHR Polling
+- XHR Multipart Streaming
+- Forever Iframe
+
+## Requirements
+
+- Node v0.1.102+
+- [Socket.IO client](http://github.com/LearnBoost/Socket.IO) to connect from the browser
+
+## How to use
+
+To run the demo:
+
+ git clone git://github.com/LearnBoost/Socket.IO-node.git socket.io-node --recursive
+ cd socket.io-node/example/
+ sudo node server.js
+
+and point your browser to http://localhost:8080. In addition to 8080, if the transport `flashsocket` is enabled, a server will be initialized to listen to requests on the port 843.
+
+### Implementing it on your project
+
+`Socket.IO` is designed not to take over an entire port or Node `http.Server` instance. This means that if you choose your HTTP server to listen on the port 80, `socket.io` can intercept requests directed to it and the normal requests will still be served.
+
+By default, the server will intercept requests that contain `socket.io` in the path / resource part of the URI. You can change this (look at the available options below).
+
+ var http = require('http'),
+ io = require('./path/to/socket.io'),
+
+ server = http.createServer(function(req, res){
+ // your normal server code
+ res.writeHeader(200, {'Content-Type': 'text/html'});
+ res.writeBody('<h1>Hello world</h1>');
+ res.finish();
+ });
+
+ // socket.io, I choose you
+ var socket = io.listen(server);
+
+ socket.on('connection', function(client){
+ // new client is here!
+ client.on('message', function(){ … })
+ client.on('disconnect', function(){ … })
+ });
+
+On the client side, you should include socket.io.js from [Socket.IO client](https://github.com/LearnBoost/Socket.IO) to connect (follow the link for an explanation of the client-side API).
+
+## Notes
+
+IMPORTANT! When checking out the git repo, make sure to include the submodules. One way to do it is:
+
+ git clone [repo] --recursive
+
+Another, once cloned
+
+ git submodule update --init --recursive
+
+## Documentation
+
+### Listener
+
+ io.listen(<http.Server>, [options])
+
+Returns: a `Listener` instance
+
+Public Properties:
+
+- *server*
+
+ The instance of _process.http.Server_
+
+- *options*
+
+ The passed in options combined with the defaults
+
+- *clients*
+
+ An array of clients. Important: disconnected clients are set to null, the array is not spliced.
+
+- *clientsIndex*
+
+ An object of clients indexed by their session ids.
+
+Methods:
+
+- *addListener(event, λ)*
+
+ Adds a listener for the specified event. Optionally, you can pass it as an option to `io.listen`, prefixed by `on`. For example: `onClientConnect: function(){}`
+
+- *removeListener(event, λ)*
+
+ Remove a listener from the listener array for the specified event.
+
+- *broadcast(message, [except])*
+
+ Broadcasts a message to all clients. There's an optional second argument which is an array of session ids or a single session id to avoid broadcasting to.
+
+Options:
+
+- *resource*
+
+ socket.io
+
+ The resource is what allows the `socket.io` server to identify incoming connections by `socket.io` clients. Make sure they're in sync.
+
+- *transports*
+
+ ['websocket', 'server-events', 'flashsocket', 'htmlfile', 'xhr-multipart', 'xhr-polling']
+
+ A list of the accepted transports.
+
+- *transportOptions*
+
+ An object of options to pass to each transport. For example `{ websocket: { closeTimeout: 8000 }}`
+
+- *log*
+
+ ƒ(){ sys.log }
+
+ The logging function. Defaults to outputting to stdout through `sys.log`
+
+Events:
+
+- *clientConnect(client)*
+
+ Fired when a client is connected. Receives the Client instance as parameter
+
+- *clientMessage(message, client)*
+
+ Fired when a message from a client is received. Receives the message and Client instance as parameter
+
+- *clientDisconnect(client)*
+
+ Fired when a client is disconnected. Receives the Client instance as parameter
+
+Important note: `this` in the event listener refers to the `Listener` instance.
+
+### Client
+
+ Client(listener, req, res)
+
+Public Properties:
+
+- *listener*
+
+ The `Listener` instance this client belongs to.
+
+- *connected*
+
+ Whether the client is connected
+
+- *connections*
+
+ Number of times the client connected
+
+Methods:
+
+- *send(message)*
+
+ Sends a message to the client
+
+- *broadcast(message)*
+
+ Sends a message to all other clients. Equivalent to Listener::broadcast(message, client.sessionId)
+
+## Protocol
+
+One of the design goals is that you should be able to implement whatever protocol you desire without `Socket.IO` getting in the way. `Socket.IO` has a minimal, unobtrusive protocol layer. It consists of two parts:
+
+* Connection handshake
+
+ This is required to simulate a full duplex socket with transports such as XHR Polling or Server-sent Events (which is a "one-way socket"). The basic idea is that the first message received from the server will be a JSON object that contains a session id that will be used for further communication exchanged between the client and the server.
+
+ The concept of session also benefits naturally full-duplex WebSocket, in the event of an accidental disconnection and a quick reconnection. Messages that the server intends to deliver to the client are cached temporarily until the reconnection.
+
+ The implementation of reconnection logic (potentially with retries) is left for the user. By default, transports that are keep-alive or open all the time (like WebSocket) have a timeout of 0 if a disconnection is detected.
+
+* Message batching
+
+ In order to optimize the resources, messages are buffered. In the event of the server trying to send multiple messages while the client is temporarily disconnected (eg: xhr polling), messages are stacked, then encoded in a lightweight way and sent to the client whenever he becomes available.
+
+Despite this extra layer, your messages are delivered unaltered to the different event listeners. You can JSON.stringify() objects, send XML, or maybe plain text.
+
+## Credits
+
+Guillermo Rauch &lt;guillermo@learnboost.com&gt;
+
+## License
+
+(The MIT License)
+
+Copyright (c) 2010 LearnBoost &lt;dev@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
63 lib/socket.io/example/chat.html
@@ -0,0 +1,63 @@
+<!doctype html>
+<html>
+ <head>
+ <title>socket.io client test</title>
+
+ <script src="/json.js"></script> <!-- for ie -->
+ <script src="/client/socket.io.js"></script>
+ </head>
+ <body>
+
+ <script>
+ io.setPath('/client/');
+
+ function message(obj){
+ var el = document.createElement('p');
+ if ('announcement' in obj) el.innerHTML = '<em>' + esc(obj.announcement) + '</em>';
+ else if ('message' in obj) el.innerHTML = '<b>' + esc(obj.message[0]) + ':</b> ' + esc(obj.message[1]);
+ document.getElementById('chat').appendChild(el);
+ document.getElementById('chat').scrollTop = 1000000;
+ }
+
+ function send(){
+ var val = document.getElementById('text').value;
+ socket.send(val);
+ message({ message: ['you', val] });
+ document.getElementById('text').value = '';
+ }
+
+ function esc(msg){
+ return msg.replace(/</g, '&lt;').replace(/>/g, '&gt;');
+ };
+
+ var socket = new io.Socket(null, {port: 8080});
+ socket.connect();
+ socket.on('message', function(obj){
+ if ('buffer' in obj){
+ document.getElementById('form').style.display='block';
+ document.getElementById('chat').innerHTML = '';
+
+ for (var i in obj.buffer) message(obj.buffer[i]);
+ } else message(obj);
+ });
+ </script>
+
+ <h1>Sample chat client</h1>
+ <div id="chat"><p>Connecting...</p></div>
+ <form id="form" onsubmit="send(); return false">
+ <input type="text" autocomplete="off" id="text"><input type="submit" value="Send">
+ </form>
+
+ <style>
+ #chat { height: 300px; overflow: auto; width: 800px; border: 1px solid #eee; font: 13px Helvetica, Arial; }
+ #chat p { padding: 8px; margin: 0; }
+ #chat p:nth-child(odd) { background: #F6F6F6; }
+ #form { width: 782px; background: #333; padding: 5px 10px; display: none; }
+ #form input[type=text] { width: 700px; padding: 5px; background: #fff; border: 1px solid #fff; }
+ #form input[type=submit] { cursor: pointer; background: #999; border: none; padding: 6px 8px; -moz-border-radius: 8px; -webkit-border-radius: 8px; margin-left: 5px; text-shadow: 0 1px 0 #fff; }
+ #form input[type=submit]:hover { background: #A2A2A2; }
+ #form input[type=submit]:active { position: relative; top: 2px; }
+ </style>
+
+ </body>
+</html>
View
18 lib/socket.io/example/json.js
@@ -0,0 +1,18 @@
+if(!this.JSON){JSON=function(){function f(n){return n<10?'0'+n:n;}
+Date.prototype.toJSON=function(){return this.getUTCFullYear()+'-'+
+f(this.getUTCMonth()+1)+'-'+
+f(this.getUTCDate())+'T'+
+f(this.getUTCHours())+':'+
+f(this.getUTCMinutes())+':'+
+f(this.getUTCSeconds())+'Z';};var m={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'};function stringify(value,whitelist){var a,i,k,l,r=/["\\\x00-\x1f\x7f-\x9f]/g,v;switch(typeof value){case'string':return r.test(value)?'"'+value.replace(r,function(a){var c=m[a];if(c){return c;}
+c=a.charCodeAt();return'\\u00'+Math.floor(c/16).toString(16)+
+(c%16).toString(16);})+'"':'"'+value+'"';case'number':return isFinite(value)?String(value):'null';case'boolean':case'null':return String(value);case'object':if(!value){return'null';}
+if(typeof value.toJSON==='function'){return stringify(value.toJSON());}
+a=[];if(typeof value.length==='number'&&!(value.propertyIsEnumerable('length'))){l=value.length;for(i=0;i<l;i+=1){a.push(stringify(value[i],whitelist)||'null');}
+return'['+a.join(',')+']';}
+if(whitelist){l=whitelist.length;for(i=0;i<l;i+=1){k=whitelist[i];if(typeof k==='string'){v=stringify(value[k],whitelist);if(v){a.push(stringify(k)+':'+v);}}}}else{for(k in value){if(typeof k==='string'){v=stringify(value[k],whitelist);if(v){a.push(stringify(k)+':'+v);}}}}
+return'{'+a.join(',')+'}';}}
+return{stringify:stringify,parse:function(text,filter){var j;function walk(k,v){var i,n;if(v&&typeof v==='object'){for(i in v){if(Object.prototype.hasOwnProperty.apply(v,[i])){n=walk(i,v[i]);if(n!==undefined){v[i]=n;}}}}
+return filter(k,v);}
+if(/^[\],:{}\s]*$/.test(text.replace(/\\./g,'@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,']').replace(/(?:^|:|,)(?:\s*\[)+/g,''))){j=eval('('+text+')');return typeof filter==='function'?walk('',j):j;}
+throw new SyntaxError('parseJSON');}};}();}
View
64 lib/socket.io/example/server.js
@@ -0,0 +1,64 @@
+var http = require('http'),
+ url = require('url'),
+ fs = require('fs'),
+ io = require('../'),
+ sys = require('sys'),
+
+send404 = function(res){
+ res.writeHead(404);
+ res.write('404');
+ res.end();
+},
+
+server = http.createServer(function(req, res){
+ // your normal server code
+ var path = url.parse(req.url).pathname;
+ switch (path){
+ case '/':
+ res.writeHead(200, {'Content-Type': 'text/html'});
+ res.write('<h1>Welcome. Try the <a href="/chat.html">chat</a> example.</h1>');
+ res.end();
+ break;
+
+ default:
+ if (/\.(js|html|swf)$/.test(path)){
+ try {
+ var swf = path.substr(-4) === '.swf';
+ res.writeHead(200, {'Content-Type': swf ? 'application/x-shockwave-flash' : ('text/' + (path.substr(-3) === '.js' ? 'javascript' : 'html'))});
+ fs.readFile(__dirname + path, swf ? 'binary' : 'utf8', function(err, data){
+ if (!err) res.write(data, swf ? 'binary' : 'utf8');
+ res.end();
+ });
+ } catch(e){
+ send404(res);
+ }
+ break;
+ }
+
+ send404(res);
+ break;
+ }
+});
+
+server.listen(8080);
+
+// socket.io, I choose you
+// simplest chat application evar
+var io = io.listen(server),
+ buffer = [];
+
+io.on('connection', function(client){
+ client.send({ buffer: buffer });
+ client.broadcast({ announcement: client.sessionId + ' connected' });
+
+ client.on('message', function(message){
+ var msg = { message: [client.sessionId, message] };
+ buffer.push(msg);
+ if (buffer.length > 15) buffer.shift();
+ client.broadcast(msg);
+ });
+
+ client.on('disconnect', function(){
+ client.broadcast({ announcement: client.sessionId + ' disconnected' });
+ });
+});
View
1 lib/socket.io/index.js
@@ -0,0 +1 @@
+module.exports = require('./lib/socket.io');
View
185 lib/socket.io/lib/socket.io/client.js
@@ -0,0 +1,185 @@
+var urlparse = require('url').parse,
+ options = require('./utils').options,
+ frame = '~m~',
+
+Client = module.exports = function(listener, req, res, options, head){
+ process.EventEmitter.call(this);
+ this.listener = listener;
+ this.options({
+ timeout: 8000,
+ heartbeatInterval: 10000,
+ closeTimeout: 0
+ }, options);
+ this.connections = 0;
+ this.connected = false;
+ this._heartbeats = 0;
+ this.upgradeHead = head;
+ this._onConnect(req, res);
+};
+
+require('sys').inherits(Client, process.EventEmitter);
+
+Client.prototype.send = function(message){
+ if (!this.connected || !(this.connection.readyState === 'open' ||
+ this.connection.readyState === 'writeOnly')){
+ return this._queue(message);
+ }
+ this._write(this._encode(message));
+ return this;
+};
+
+Client.prototype.broadcast = function(message){
+ if (!('sessionId' in this)) return this;
+ this.listener.broadcast(message, this.sessionId);
+ return this;
+};
+
+Client.prototype._onMessage = function(data){
+ var messages = this._decode(data);
+ if (messages === false) return this.listener.options.log('Bad message received from client ' + this.sessionId);
+ for (var i = 0, l = messages.length, frame; i < l; i++){
+ frame = messages[i].substr(0, 3);
+ switch (frame){
+ case '~h~':
+ return this._onHeartbeat(messages[i].substr(3));
+ case '~j~':
+ messages[i] = JSON.parse(messages[i].substr(3));
+ break;
+ }
+ this.emit('message', messages[i]);
+ this.listener._onClientMessage(messages[i], this);
+ }
+};
+
+Client.prototype._onConnect = function(req, res){
+ var self = this;
+ this.request = req;
+ this.response = res;
+ this.connection = this.request.connection;
+ if (this._disconnectTimeout) clearTimeout(this._disconnectTimeout);
+};
+
+Client.prototype._encode = function(messages){
+ var ret = '', message,
+ messages = Array.isArray(messages) ? messages : [messages];
+ for (var i = 0, l = messages.length; i < l; i++){
+ message = messages[i] === null || messages[i] === undefined ? '' : stringify(messages[i]);
+ ret += frame + message.length + frame + message;
+ }
+ return ret;
+};
+
+Client.prototype._decode = function(data){
+ var messages = [], number, n;
+ do {
+ if (data.substr(0, 3) !== frame) return messages;
+ data = data.substr(3);
+ number = '', n = '';
+ for (var i = 0, l = data.length; i < l; i++){
+ n = Number(data.substr(i, 1));
+ if (data.substr(i, 1) == n){
+ number += n;
+ } else {
+ data = data.substr(number.length + frame.length)
+ number = Number(number);
+ break;
+ }
+ }
+ messages.push(data.substr(0, number)); // here
+ data = data.substr(number);
+ } while(data !== '');
+ return messages;
+};
+
+Client.prototype._payload = function(){
+ var payload = [];
+
+ this.connections++;
+ this.connected = true;
+
+ if (!this.handshaked){
+ this._generateSessionId();
+ payload.push(this.sessionId);
+ this.handshaked = true;
+ }
+
+ payload = payload.concat(this._writeQueue || []);
+ this._writeQueue = [];
+
+ if (payload.length) this._write(this._encode(payload));
+ if (this.connections === 1) this.listener._onClientConnect(this);
+
+ if (this.options.timeout) this._heartbeat();
+};
+
+Client.prototype._heartbeat = function(){
+ var self = this;
+ setTimeout(function(){
+ self.send('~h~' + ++self._heartbeats);
+ self._heartbeatTimeout = setTimeout(function(){
+ self._onClose();
+ }, self.options.timeout);
+ }, self.options.heartbeatInterval);
+};
+
+Client.prototype._onHeartbeat = function(h){
+ if (h == this._heartbeats){
+ clearTimeout(this._heartbeatTimeout);
+ this._heartbeat();
+ }
+};
+
+Client.prototype._onClose = function(){
+ if (this.connected){
+ var self = this;
+ if ('_heartbeatTimeout' in this) clearTimeout(this._heartbeatTimeout);
+ this.connected = false;
+ this._disconnectTimeout = setTimeout(function(){
+ self._onDisconnect();
+ }, this.options.closeTimeout);
+ }
+};
+
+Client.prototype._onDisconnect = function(){
+ if (!this.finalized){
+ this._writeQueue = [];
+ this.connected = false;
+ this.finalized = true;
+ if (this.handshaked){
+ this.emit('disconnect');
+ this.listener._onClientDisconnect(this);
+ }
+ }
+};
+
+Client.prototype._queue = function(message){
+ if (!('_writeQueue' in this)){
+ this._writeQueue = [];
+ }
+ this._writeQueue.push(message);
+ return this;
+};
+
+Client.prototype._generateSessionId = function(){
+ if (this.sessionId) return this.listener.options.log('This client already has a session id');
+ this.sessionId = Math.random().toString().substr(2);
+ return this;
+};
+
+Client.prototype._verifyOrigin = function(origin){
+ var parts = urlparse(origin), origins = this.listener.options.origins;
+ return origins.indexOf('*:*') !== -1 ||
+ origins.indexOf(parts.host + ':' + parts.port) !== -1 ||
+ origins.indexOf(parts.host + ':*') !== -1 ||
+ origins.indexOf('*:' + parts.port) !== -1;
+};
+
+for (var i in options) Client.prototype[i] = options[i];
+
+function stringify(message){
+ if (Object.prototype.toString.call(message) == '[object Object]'){
+ return '~j~' + JSON.stringify(message);
+ } else {
+ return String(message);
+ }
+};
View
4 lib/socket.io/lib/socket.io/index.js
@@ -0,0 +1,4 @@
+exports.Listener = require('./listener');
+exports.listen = function(server, options){
+ return new exports.Listener(server, options);
+};
View
124 lib/socket.io/lib/socket.io/listener.js
@@ -0,0 +1,124 @@
+var url = require('url'),
+ sys = require('sys'),
+ options = require('./utils').options,
+ Client = require('./client'),
+ transports = {
+ 'flashsocket': require('./transports/flashsocket'),
+ 'htmlfile': require('./transports/htmlfile'),
+ 'websocket': require('./transports/websocket'),
+ 'xhr-multipart': require('./transports/xhr-multipart'),
+ 'xhr-polling': require('./transports/xhr-polling')
+ },
+
+Listener = module.exports = function(server, options){
+ process.EventEmitter.call(this);
+ var self = this;
+ this.server = server;
+ this.options({
+ origins: '*:*',
+ resource: 'socket.io',
+ transports: ['websocket', 'flashsocket', 'htmlfile', 'xhr-multipart', 'xhr-polling'],
+ transportOptions: {
+ 'xhr-polling': {
+ timeout: null, // no heartbeats for polling
+ closeTimeout: 8000,
+ duration: 20000
+ }
+ },
+ log: function(message){
+ require('sys').log(message);
+ }
+ }, options);
+ this.clients = [];
+ this.clientsIndex = {};
+
+ var listeners = this.server.listeners('request');
+ this.server.removeAllListeners('request');
+
+ this.server.addListener('request', function(req, res){
+ if (self.check(req, res)) return;
+ for (var i = 0; i < listeners.length; i++){
+ listeners[i].call(this, req, res);
+ }
+ });
+
+ this.server.addListener('upgrade', function(req, socket, head){
+ if (!self.check(req, socket, true, head)){
+ socket.destroy();
+ }
+ });
+
+ for (var i in transports){
+ if ('init' in transports[i]) transports[i].init(this);
+ }
+
+ this.options.log('socket.io ready - accepting connections');
+};
+
+sys.inherits(Listener, process.EventEmitter);
+for (var i in options) Listener.prototype[i] = options[i];
+
+Listener.prototype.broadcast = function(message, except){
+ for (var i = 0, l = this.clients.length; i < l; i++){
+ if (this.clients[i] && (!except || [].concat(except).indexOf(this.clients[i].sessionId) == -1)){
+ this.clients[i].send(message);
+ }
+ }
+ return this;
+};
+
+Listener.prototype.check = function(req, res, httpUpgrade, head){
+ var path = url.parse(req.url).pathname, parts, cn;
+ if (path.indexOf('/' + this.options.resource) === 0){
+ parts = path.substr(1).split('/');
+ if (parts[2]){
+ cn = this._lookupClient(parts[2]);
+ if (cn){
+ cn._onConnect(req, res);
+ } else {
+ req.connection.end();
+ this.options.log('Couldnt find client with session id "' + parts[2] + '"');
+ }
+ } else {
+ this._onConnection(parts[1], req, res, httpUpgrade, head);
+ }
+ return true;
+ }
+ return false;
+};
+
+Listener.prototype._lookupClient = function(sid){
+ return this.clientsIndex[sid];
+};
+
+Listener.prototype._onClientConnect = function(client){
+ if (!(client instanceof Client) || !client.sessionId){
+ return this.options.log('Invalid client');
+ }
+ client.i = this.clients.length;
+ this.clients.push(client);
+ this.clientsIndex[client.sessionId] = client;
+ this.options.log('Client '+ client.sessionId +' connected');
+ this.emit('clientConnect', client);
+ this.emit('connection', client);
+};
+
+Listener.prototype._onClientMessage = function(data, client){
+ this.emit('clientMessage', data, client);
+};
+
+Listener.prototype._onClientDisconnect = function(client){
+ this.clientsIndex[client.sessionId] = null;
+ this.clients[client.i] = null;
+ this.options.log('Client '+ client.sessionId +' disconnected');
+ this.emit('clientDisconnect', client);
+};
+
+Listener.prototype._onConnection = function(transport, req, res, httpUpgrade, head){
+ if (this.options.transports.indexOf(transport) === -1 || (httpUpgrade && !transports[transport].httpUpgrade)){
+ httpUpgrade ? res.destroy() : req.connection.destroy();
+ return this.options.log('Illegal transport "'+ transport +'"');
+ }
+ this.options.log('Initializing client with transport "'+ transport +'"');
+ new transports[transport](this, req, res, this.options.transportOptions[transport], head);
+};
View
39 lib/socket.io/lib/socket.io/transports/flashsocket.js
@@ -0,0 +1,39 @@
+var net = require('net'),
+ WebSocket = require('./websocket'),
+ listeners = [],
+ netserver,
+
+Flashsocket = module.exports = function(){
+ WebSocket.apply(this, arguments);
+};
+
+require('sys').inherits(Flashsocket, WebSocket);
+
+Flashsocket.httpUpgrade = true;
+
+Flashsocket.init = function(listener){
+ listeners.push(listener);
+ listener.server.on('close', function(){
+ try {
+ netserver.close();
+ } catch(e){}
+ });
+};
+
+try {
+ netserver = net.createServer(function(socket){
+ socket.write('<?xml version="1.0"?>\n');
+ socket.write('<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">\n');
+ socket.write('<cross-domain-policy>\n');
+
+ listeners.forEach(function(l){
+ [].concat(l.options.origins).forEach(function(origin){
+ var parts = origin.split(':');
+ socket.write('<allow-access-from domain="' + parts[0] + '" to-ports="'+ parts[1] +'"/>\n');
+ });
+ });
+
+ socket.write('</cross-domain-policy>\n');
+ socket.end();
+ }).listen(843);
+} catch(e){}
View
48 lib/socket.io/lib/socket.io/transports/htmlfile.js
@@ -0,0 +1,48 @@
+var Client = require('../client'),
+ qs = require('querystring'),
+
+HTMLFile = module.exports = function(){
+ Client.apply(this, arguments);
+};
+
+require('sys').inherits(HTMLFile, Client);
+
+HTMLFile.prototype._onConnect = function(req, res){
+ var self = this, body = '';
+ switch (req.method){
+ case 'GET':
+ Client.prototype._onConnect.apply(this, [req, res]);
+ this.request.connection.addListener('close', function(){ self._onClose(); });
+ this.response.useChunkedEncodingByDefault = true;
+ this.response.shouldKeepAlive = true;
+ this.response.writeHead(200, {
+ 'Content-Type': 'text/html',
+ 'Connection': 'keep-alive',
+ 'Transfer-Encoding': 'chunked'
+ });
+ this.response.write('<html><body>' + new Array(244).join(' '));
+ if ('flush' in this.response) this.response.flush();
+ this._payload();
+ break;
+
+ case 'POST':
+ req.addListener('data', function(message){
+ body += message;
+ });
+ req.addListener('end', function(){
+ try {
+ var msg = qs.parse(body);
+ self._onMessage(msg.data);
+ } catch(e){}
+ res.writeHead(200, {'Content-Type': 'text/plain'});
+ res.write('ok');
+ res.end();
+ });
+ break;
+ }
+};
+
+HTMLFile.prototype._write = function(message){
+ this.response.write('<script>parent.s._('+ JSON.stringify(message) +', document);</script>'); //json for escaping
+ if ('flush' in this.response) this.response.flush();
+};
View
142 lib/socket.io/lib/socket.io/transports/websocket.js
@@ -0,0 +1,142 @@
+var Client = require('../client'),
+ url = require('url'),
+ Buffer = require('buffer').Buffer,
+ crypto = require('crypto'),
+
+WebSocket = module.exports = function(){
+ Client.apply(this, arguments);
+};
+
+require('sys').inherits(WebSocket, Client);
+
+WebSocket.prototype._onConnect = function(req, socket){
+ var self = this, headers = [];
+ this.request = req;
+ this.connection = socket;
+ this.data = '';
+
+ if (this.request.headers.upgrade !== 'WebSocket' || !this._verifyOrigin(this.request.headers.origin)){
+ this.listener.options.log('WebSocket connection invalid');
+ this.connection.writeHead(500);
+ this.connection.end();
+ return false;
+ }
+
+ this.connection.setTimeout(0);
+ this.connection.setEncoding('utf8');
+ this.connection.setNoDelay(true);
+
+ if ('sec-websocket-key1' in this.request.headers){
+ this.draft = 76;
+ }
+
+ if (this.draft == 76){
+ var origin = this.request.headers.origin;
+
+ headers = [
+ 'HTTP/1.1 101 WebSocket Protocol Handshake',
+ 'Upgrade: WebSocket',
+ 'Connection: Upgrade',
+ 'Sec-WebSocket-Origin: ' + (origin || 'null'),
+ 'Sec-WebSocket-Location: ws://' + this.request.headers.host + this.request.url
+ ];
+
+ if ('sec-websocket-protocol' in this.request.headers){
+ headers.push('Sec-WebSocket-Protocol: ' + this.request.headers['sec-websocket-protocol']);
+ }
+ } else {
+
+ headers = [
+ 'HTTP/1.1 101 Web Socket Protocol Handshake',
+ 'Upgrade: WebSocket',
+ 'Connection: Upgrade',
+ 'WebSocket-Origin: ' + this.request.headers.origin,
+ 'WebSocket-Location: ws://' + this.request.headers.host + this.request.url
+ ];
+
+ try {
+ this.connection.write(headers.concat('', '').join('\r\n'));
+ } catch(e){
+ this._onClose();
+ }
+ }
+
+ this.connection.addListener('end', function(){
+ self._onClose();
+ });
+
+ this.connection.addListener('data', function(data){
+ self._handle(data);
+ });
+
+ if (this._proveReception(headers)) this._payload();
+};
+
+WebSocket.prototype._handle = function(data){
+ var chunk, chunks, chunk_count;
+ this.data += data;
+ chunks = this.data.split('\ufffd');
+ chunk_count = chunks.length - 1;
+ for (var i = 0; i < chunk_count; i++){
+ chunk = chunks[i];
+ if (chunk[0] !== '\u0000'){
+ this.listener.options.log('Data incorrectly framed by UA. Dropping connection');
+ this.connection.end();
+ return false;
+ }
+ this._onMessage(chunk.slice(1));
+ }
+ this.data = chunks[chunks.length - 1];
+};
+
+// http://www.whatwg.org/specs/web-apps/current-work/complete/network.html#opening-handshake
+WebSocket.prototype._proveReception = function(headers){
+ var k1 = this.request.headers['sec-websocket-key1'],
+ k2 = this.request.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){
+ this.listener.options.log('Invalid WebSocket key: "' + k + '". Dropping connection');
+ this.connection.writeHead(500);
+ this.connection.end();
+ return false;
+ }
+
+ n /= spaces;
+
+ md5.update(String.fromCharCode(
+ n >> 24 & 0xFF,
+ n >> 16 & 0xFF,
+ n >> 8 & 0xFF,
+ n & 0xFF));
+ });
+
+ md5.update(this.upgradeHead.toString('binary'));
+
+ try {
+ this.connection.write(headers.concat('', '').join('\r\n') + md5.digest('binary'), 'binary');
+ } catch(e){
+ this._onClose();
+ }
+ }
+
+ return true;
+};
+
+WebSocket.prototype._write = function(message){
+ try {
+ this.connection.write('\u0000', 'binary');
+ this.connection.write(message, 'utf8');
+ this.connection.write('\uffff', 'binary');
+ } catch(e){
+ this._onClose();
+ }
+};
+
+WebSocket.httpUpgrade = true;
View
63 lib/socket.io/lib/socket.io/transports/xhr-multipart.js
@@ -0,0 +1,63 @@
+var Client = require('../client'),
+ qs = require('querystring'),
+
+Multipart = module.exports = function(){
+ Client.apply(this, arguments);
+};
+
+require('sys').inherits(Multipart, Client);
+
+Multipart.prototype._onConnect = function(req, res){
+ var self = this, body = '', headers = {};
+ // https://developer.mozilla.org/En/HTTP_Access_Control
+ if (req.headers.origin && this._verifyOrigin(req.headers.origin)){
+ headers['Access-Control-Allow-Origin'] = req.headers.origin;
+ headers['Access-Control-Allow-Credentials'] = 'true';
+ }
+ if (typeof req.headers['access-control-request-method'] !== 'undefined'){
+ // CORS preflight message
+ headers['Access-Control-Allow-Methods'] = req.headers['access-control-request-method'];
+ res.writeHead(200, headers);
+ res.write('ok');
+ res.end();
+ return;
+ }
+ switch (req.method){
+ case 'GET':
+ Client.prototype._onConnect.apply(this, [req, res]);
+ headers['Content-Type'] = 'multipart/x-mixed-replace;boundary="socketio"';
+ headers['Connection'] = 'keep-alive';
+ this.request.connection.addListener('end', function(){ self._onClose(); });
+ this.response.useChunkedEncodingByDefault = false;
+ this.response.shouldKeepAlive = true;
+ this.response.writeHead(200, headers);
+ this.response.write("--socketio\n");
+ if ('flush' in this.response) this.response.flush();
+ this._payload();
+ break;
+
+ case 'POST':
+ headers['Content-Type'] = 'text/plain';
+ req.addListener('data', function(message){
+ body += message.toString();
+ });
+ req.addListener('end', function(){
+ try {
+ var msg = qs.parse(body);
+ self._onMessage(msg.data);
+ } catch(e){}
+ res.writeHead(200, headers);
+ res.write('ok');
+ res.end();
+ body = '';
+ });
+ break;
+ }
+};
+
+Multipart.prototype._write = function(message){
+ this.response.write("Content-Type: text/plain" + (message.length === 1 && message.charCodeAt(0) === 6 ? "; charset=us-ascii" : "") + "\n\n");
+ this.response.write(message + "\n");
+ this.response.write("--socketio\n");
+ if ('flush' in this.response) this.response.flush();
+};
View
52 lib/socket.io/lib/socket.io/transports/xhr-polling.js
@@ -0,0 +1,52 @@
+var Client = require('../client'),
+ qs = require('querystring'),
+
+Polling = module.exports = function(){
+ Client.apply(this, arguments);
+};
+
+require('sys').inherits(Polling, Client);
+
+Polling.prototype._onConnect = function(req, res){
+ var self = this, body = '';
+ switch (req.method){
+ case 'GET':
+ Client.prototype._onConnect.apply(this, [req, res]);
+ this.request.connection.addListener('end', function(){ self._onClose(); });
+ this._closeTimeout = setTimeout(function(){
+ self._write('');
+ }, this.options.duration);
+ this._payload();
+ break;
+
+ case 'POST':
+ req.addListener('data', function(message){
+ body += message;
+ });
+ req.addListener('end', function(){
+ try {
+ // optimization: just strip first 5 characters here?
+ var msg = qs.parse(body);
+ self._onMessage(msg.data);
+ } catch(e){}
+ res.writeHead(200, {'Content-Type': 'text/plain'});
+ res.write('ok');
+ res.end();
+ });
+ break;
+ }
+};
+
+Polling.prototype._write = function(message){
+ if (this._closeTimeout) clearTimeout(this._closeTimeout);
+ var headers = {'Content-Type': 'text/plain', 'Content-Length': message.length};
+ // https://developer.mozilla.org/En/HTTP_Access_Control
+ if (this.request.headers.origin && this._verifyOrigin(this.request.headers.origin)){
+ headers['Access-Control-Allow-Origin'] = this.request.headersorigin;
+ if (this.request.headers.cookie) headers['Access-Control-Allow-Credentials'] = 'true';
+ }
+ this.response.writeHead(200, headers);
+ this.response.write(message);
+ this.response.end();
+ this._onClose();
+};
View
10 lib/socket.io/lib/socket.io/utils.js
@@ -0,0 +1,10 @@
+exports.options = {
+ options: function(options, merge){
+ this.options = exports.merge(options || {}, merge || {});
+ }
+};
+
+exports.merge = function(source, merge){
+ for (var i in merge) source[i] = merge[i];
+ return source;
+};
View
28 lib/socket.io/tests/client.test.js
@@ -0,0 +1,28 @@
+var listener = require('socket.io/listener'),
+ Client = require('socket.io/client');
+
+module.exports = {
+ 'test decoding': function(assert){
+ var client = new Client(listener, {}, {}),
+ decoded = client._decode('~m~5~m~abcde' + '~m~9~m~123456789');
+ assert.equal(decoded.length, 2);
+ assert.equal(decoded[0], 'abcde');
+ assert.equal(decoded[1], '123456789');
+ },
+
+ 'test decoding of bad framed messages': function(assert){
+ var client = new Client(listener, {}, {}),
+ decoded = client._decode('~m~5~m~abcde' + '~m\uffsdaasdfd9~m~1aaa23456789');
+ assert.equal(decoded.length, 1);
+ assert.equal(decoded[0], 'abcde');
+ assert.equal(decoded[1], undefined);
+ },
+
+ 'test encoding': function(assert){
+ var client = new Client(listener, {}, {});
+ assert.equal(client._encode(['abcde', '123456789']), '~m~5~m~abcde' + '~m~9~m~123456789');
+ assert.equal(client._encode('asdasdsad'), '~m~9~m~asdasdsad');
+ assert.equal(client._encode(''), '~m~0~m~');
+ assert.equal(client._encode(null), '~m~0~m~');
+ }
+};
View
17 lib/socket.io/tests/io.test.js
@@ -0,0 +1,17 @@
+var io = require('./../'),
+ Listener = io.Listener,
+ port = 8080;
+ Client = require('./../lib/socket.io/client'),
+ WebSocket = require('./support/node-websocket-client/lib/websocket').WebSocket,
+
+module.exports = {
+
+ 'test server initialization': function(assert){
+ var server = require('http').createServer(function(){}), sio;
+ server.listen(8080);
+ sio = io.listen(server);
+ assert.ok(sio instanceof Listener);
+ server.close();
+ }
+
+};
View
35 lib/socket.io/tests/transports.websocket.js
@@ -0,0 +1,35 @@
+var io = require('./../'),
+ Listener = io.Listener,
+ Client = require('./../lib/socket.io/client'),
+ WebSocket = require('./support/node-websocket-client/lib/websocket').WebSocket;
+
+module.exports = {
+
+ 'test connection and handshake': function(assert){
+ var server = require('http').createServer(function(){}), sio, client, clientCount, close;
+ server.listen(8081);
+
+ sio = io.listen(server);
+ client;
+ clientCount = 0;
+ close = function(){
+ client.close();
+ server.close();
+ assert.ok(clientCount, 1);
+ };
+
+ server.listen(port, function(){
+ client = new WebSocket('ws://localhost:8081/socket.io/websocket', 'borf');
+ client.onmessage = function(){
+ console.log('test');
+ };
+ });
+
+ sio.on('connection', function(client){
+ console.log('test');
+ clientCount++;
+ assert.ok(client instanceof Client);
+ });
+ }
+
+};
View
9 support.js
@@ -1,2 +1,9 @@
-require.paths.unshift('./lib/connect/lib', './lib/express/lib', './lib/ejs/lib');
+require.paths.unshift(
+
+ , './lib/connect/lib'
+ , './lib/express/lib'
+ , './lib/ejs/lib'
+ , './lib/socket.io/lib'
+
+);
express = require('express');

0 comments on commit 8a9712a

Please sign in to comment.