Skip to content
Browse files

Lots of changes.

- TCP client "support" removed (for now?)
- Factored out client storage into separate storage providers -- which storage provider to use is configurable during creation of a grappler server
- Data from client -> server is now streamed instead of buffered (especially in the case of websockets)
- Move constants and other common things into a separate file
- Echo server re-done -- now includes a generic/reusable client-side js function to automatically choose the best available transport and provide a common API
-
  • Loading branch information...
1 parent 93cff46 commit 56af9ba7c80209c36f25bec28f2c7885c8213815 @mscdex committed Mar 17, 2011
View
126 README.md
@@ -1,55 +1,57 @@
Grappler
========
-Grappler is a minimalistic server for "comet" and TCP connections that exposes a single, consistent API across all transports.
-Grappler currently supports the following transports (each with a list of currently supported browsers):
+Grappler is a minimalistic server for "comet" connections that exposes a single, consistent API across all transports.
+Grappler currently supports the following transports (each with a list of currently known supported browsers):
-- WebSockets (with Flash policy support -- watches for policy requests on the same port as the grappler server)
- - Firefox 4, Chrome 4, Safari 5, or any browser that supports at least Flash 9.x
-- XHR Long Polling
- - Any browser that supports XMLHttpRequest*
-- XHR Multipart Streaming
- - Firefox 3
-- Server-Sent Events
- - Chrome 6, Safari 5, Opera 9.x-10.x (DOM only)
-- Plain TCP connections (Not yet implemented)
+* WebSockets (with Flash policy support -- watches for policy requests on the same port as the grappler server)
+ * Firefox 4, Chrome 4+, Safari 5, or any browser that supports at least Flash 9.x
+* XHR Multipart Streaming
+ * Firefox 3+, Safari 5 (maybe 4.0 also?), Chrome 1+
+* Server-Sent Events
+ * Chrome 6+, Safari 5, Opera 9.x-10.x (DOM only)
+* XHR Long Polling
+ * Any browser that supports XMLHttpRequest*
-\* - Some browsers' XMLHttpRequest implementations contain unexpected quirks (i.e. the built-in web browser for Android 1.6)
+\* - Some browsers' XMLHttpRequest implementations contain unexpected quirks (e.g. the built-in web browser for Android 1.6)
Requirements
============
-- Node.JS v0.1.100+
-- A client supporting one of the aforementioned transports.
-- For HTTP (non-WebSocket) clients, cookies must be enabled for clients ONLY if they are going to send messages (i.e. via POST) to the server.
+* Node.JS (tested with v0.4.2 -- may work with older versions)
+* A client supporting one of the aforementioned transports.
+* For HTTP (non-WebSocket) clients, cookies must be enabled for clients ONLY if they are going to send messages (i.e. via POST) to the server.
-Example
-=======
+Example (broadcast echo)
+========================
-Run example/server.js.
-Visit the example server's test page in your browser: http://serverip:8080/test
+1. Run examples/echo/server.js.
+2. Visit the demo server's page in one or more browsers: http://127.0.0.1:8080/demo.htm
API
===
-Grappler exports a few objects, with the main object being: `Server`.
-The others include the `LOG` and `STATE` objects, which contain constants used for when logging messages and representing the state of a client respectively.
+Grappler exports one main object: `Server`.
+
+lib/common.js exports `LOG` and `STATE` objects, which contain constants used for
+when logging messages and checking the state of a client respectively.
The `LOG` object is:
+
{
- INFO: 1,
- WARN: 2,
- ERROR: 3
+ INFO: 1,
+ WARN: 2,
+ ERROR: 3
}
The `STATE` object is:
+
{
- ACCEPTED: 1, // internal use only
- TEMP: 2, // internal use only
- PROTO_HTTP: 4, // client is HTTP-based
- PROTO_WEBSOCKET: 8, // client is WebSocket-based
- PROTO_TCP: 16 // client is plain TCP-based
- }
+ ACCEPTED: 1, // internal use only
+ TEMP: 2, // internal use only
+ PROTO_HTTP: 4, // client is HTTP-based
+ PROTO_WEBSOCKET: 8 // client is WebSocket-based
+ }
## Server
@@ -58,46 +60,44 @@ The `STATE` object is:
Creates a new instance of a grappler server.
`options` is an object with the following default values:
+
{
// A callback for receiving "debug" information. It is called with two arguments: message and message level.
- // Message level is one of the values in the `LOG` object.
+ // Message level is one of the values in the `LOG` object.
logger: function(msg, msgLevel) {},
- // A string or array of strings which denote who is allowed to connect to this grappler Server instance.
- // The format of the string is: "hostname:port", "hostname", or an asterisk substituted for either the hostname
- // or port, or both, to act as a wildcard.
- origins: "*:*",
-
- // An integer value in milliseconds representing the interval in which a ping is sent to an HTTP client for those
- // transports that need to do so.
- pingInterval: 3000
- }
+ // A string or array of strings which denote who is allowed to connect to this grappler Server instance.
+ // The format of the string is: "hostname:port", "hostname", or an asterisk substituted for either the hostname
+ // or port, or both, to act as a wildcard.
+ origins: "*:*",
+
+ // An integer value in milliseconds representing the interval in which a ping is sent to an HTTP client for those
+ // transports that need to do so.
+ pingInterval: 3000,
+
+ // A storage provider used to store client objects. The default is to use 'object', a simple hash. Other available
+ // storage providers can be found in lib/storage. The value here is the name without the 'storage' prefix and file extension.
+ storage: 'object'
+ }
`fnHandleNormalHTTP` is a callback which is able to override grappler for an incoming HTTP connection.
If no headers are sent, then grappler takes control of the connection. The arguments provided to this callback are the
-same for `http.Server`'s `request` event, that is the http.ServerRequest and http.ServerResponse objects. It should be
+same for `http.Server`'s `request` event, that is: http.ServerRequest and http.ServerResponse objects. It should be
noted that if you want grappler to automatically handle all incoming HTTP connections but want to specify a callback
-for `fnAcceptClient`, you need to specify `null` or `false` for `fnHandleNormalHTTP`.
+for `fnAcceptClient`, you need to specify a `false` value for `fnHandleNormalHTTP`.
-`fnAcceptClient` is a callback that is executed the moment a client connects (even before they are deemed an HTTP or plain TCP
-client). The main purpose of this callback is to have the chance to immediately deny a client further access to the grappler server.
-For example, your application may maintain a blacklist or may automatically blacklist/throttle back a certain IP after x connections in y time.
-If this callback returns false, the connection will automatically be dropped, otherwise the connection will be permitted.
+`fnAcceptClient` is a callback that is executed the moment a client connects. The main purpose of this callback is to
+have the chance to immediately deny a client further access to the grappler server. For example, your application may
+maintain a blacklist or may automatically blacklist/throttle back a certain IP after x connections in a certain amount of time.
+If this callback returns `false`, the connection will automatically be dropped, otherwise the connection will be permitted.
The callback receives one argument, which is the `net.Stream` object representing the connection.
### Event: connection
`function(client) { }`
This event is emitted every time a new client has successfully connected to the system.
-`client` is an instance of `Client`.
-
-### Event: data
-
-`function(data, client) { }`
-
-This event is emitted when a client sends data to the server.
-`data` is a Buffer containing the data and `client` is the `Client` who sent it.
+`client` is an instance of `HttpClient`.
### Event: error
@@ -109,19 +109,19 @@ Emitted when an unexpected error occurs.
Starts the server listening on the specified `port` and `host`. If `host` is omitted, the server will listen on any IP address.
-### broadcast(data)
+### broadcast(data[, encoding])
-Sends `data` to every connected client.
+Sends `data` to every connected client using an optional `encoding`.
### shutdown()
Shuts down the server by no longer listening for incoming connections and severing any existing client connections.
-## Client
+## HttpClient
-There is one other important object that is used in grappler, and that is the `Client` object.
-`Client` represents a user connected to the server and can be used to send that user data.
+There is one other important object that is used in grappler, and that is the `HttpClient` object.
+`HttpClient` represents a user connected to the server and can be used to interact with that user.
### Event: drain
@@ -137,22 +137,22 @@ Emitted when the client has disconnected.
### state
-A bit field containing the current state of the client. See the aforementioned `STATE` object for valid bits.
+A bit field containing the current state of the client. See the previously mentioned `STATE` object for valid bits.
### remoteAddress
The IP address of the client.
-### write(data, [encoding])
+### write(data[, encoding])
Sends `data` using an optional `encoding` to the client.
This function returns `true` if the entire data was flushed successfully to the kernel
buffer. Otherwise, it will return `false` if all or part of the data was queued in user memory.
`drain` will be emitted when the kernel buffer is free again.
-### broadcast(data)
+### broadcast(data[, encoding])
-Sends `data` to every connected client except itself.
+Sends `data` to all other connected clients using an optional `encoding`.
### disconnect()
View
95 example/server.js
@@ -1,95 +0,0 @@
-var sys = require('sys'),
- fs = require('fs'),
- grappler = require('../lib/grappler'),
- testPage, jsFlashWS, swf, address = "", port = 8080;
-
-// Load the demo page
-try {
- testPage = fs.readFileSync('test.htm');
-} catch (err) {
- sys.puts('An error occurred while reading \'test.htm\': ' + err);
- process.exit(1);
-}
-
-// Load the minified javascript needed to add support for Flash WebSockets
-try {
- jsFlashWS = fs.readFileSync('flashws.js');
-} catch (err) {
- sys.puts('An error occurred while reading \'flashws.js\': ' + err);
- process.exit(1);
-}
-
-// Load the Flash WebSocket file itself
-try {
- swf = fs.readFileSync('WebSocketMain.swf');
-} catch (err) {
- sys.puts('An error occurred while reading \'WebSocketMain.swf\': ' + err);
- process.exit(1);
-}
-
-// Create a new instance of a grappler server
-var echoServer = new grappler.Server({
- logger: function(msg, level) {
- if (level == grappler.LOG.INFO)
- msg = 'INFO: ' + msg;
- else if (level == grappler.LOG.WARN)
- msg = 'WARN: ' + msg;
- else
- msg = 'ERROR: ' + msg;
- sys.debug(msg);
- }
-}, function(req, res) { // HTTP override function that lets us decide to handle requests instead of grappler
- // Lazily determine the server's reachable IP address and port
- if (address.length == 0) {
- address = req.headers.host;
- testPage = testPage.toString().replace(/__address__/g, address);
- }
- switch (req.url) {
- case "/test":
- res.writeHead(200, { 'Connection': 'close', 'Content-Type': 'text/html', 'Content-Length': testPage.length });
- res.end(testPage);
- break;
- case "/flashws.js":
- res.writeHead(200, { 'Connection': 'close', 'Content-Type': 'text/javascript', 'Content-Length': jsFlashWS.length });
- res.end(jsFlashWS);
- break;
- case "/WebSocketMain.swf":
- res.writeHead(200, { 'Connection': 'close', 'Content-Type': 'application/x-shockwave-flash', 'Content-Length': swf.length });
- res.end(swf);
- break;
- case "/favicon.ico":
- res.writeHead(404, { 'Connection': 'close' });
- res.end();
- break;
- }
-});
-
-// Listen for an incoming connection
-echoServer.addListener('connection', function(client) {
- var type;
- if (client.state & grappler.STATE.PROTO_WEBSOCKET)
- type = "WebSocket";
- else if (client.state & grappler.STATE.PROTO_HTTP)
- type = "HTTP";
- else if (client.state & grappler.STATE.PROTO_TCP)
- type = "TCP";
- else
- type = "Unknown";
-
- sys.puts(type + ' client connected from ' + client.remoteAddress);
-
- client.addListener('data', function(buffer, from) {
- sys.puts('Received the following from client @ ' + from.remoteAddress + ': ' + buffer.toString());
- });
- client.addListener('close', function() {
- sys.puts(type + ' client disconnected from ' + client.remoteAddress);
- });
-});
-echoServer.addListener('data', function(buffer, client) {
- sys.puts('Received the following from client @ ' + client.remoteAddress + ': ' + buffer.toString());
- // Echo back to the user
- client.write(buffer);
-});
-
-echoServer.listen(port);
-sys.puts('Echo server started on port ' + port + '.');
View
0 example/WebSocketMain.swf → examples/connect/WebSocketMain.swf
File renamed without changes.
View
33 examples/connect/app.js
@@ -0,0 +1,33 @@
+var sys = require('sys'),
+ net = require('net'),
+ grappler = require('../../lib/grappler'),
+ connect = require('connect');
+
+var connect_server = connect.createServer();
+
+connect_server.use('/',
+ function(req, resp, next) {
+ if( resp instanceof net.Stream ) {
+ return;
+ }
+ next();
+ },
+ connect.staticProvider()
+);
+
+
+var server = new grappler.Server({
+ logger: function(msg, level) {
+ if (level == grappler.LOG.INFO)
+ msg = 'INFO: ' + msg;
+ else if (level == grappler.LOG.WARN)
+ msg = 'WARN: ' + msg;
+ else
+ msg = 'ERROR: ' + msg;
+ sys.debug(msg);
+ }
+}, function(req, resp) {
+ connect_server.handle(req, resp, function() {});
+});
+
+server.listen(3001);
View
0 example/flashws.js → examples/connect/flashws.js
File renamed without changes.
View
6 example/test.htm → examples/connect/index.html
@@ -1,9 +1,9 @@
<html>
<head>
<title>Grappler test page</title>
- <script type="text/javascript" src="http://__address__/flashws.js"></script>
+ <script type="text/javascript" src="flashws.js"></script>
<script type="text/javascript">
- var address = "__address__", empty = function() {}, ws, sse, lp, mp;
+ var address = "localhost:3001", empty = function() {}, ws, sse, lp, mp;
WEB_SOCKET_SWF_LOCATION = "WebSocketMain.swf";
WEB_SOCKET_DEBUG = true;
@@ -264,4 +264,4 @@
</form>
<div id="lp_log" class="log"></div>
</body>
-</html>
+</html>
View
BIN examples/echo/WebSocketMain.swf
Binary file not shown.
View
52 examples/echo/demo.htm
@@ -0,0 +1,52 @@
+<html>
+ <head>
+ <title>Grappler Broadcast Echo Demo</title>
+ <script type="text/javascript" src="flashws.js"></script>
+ <script type="text/javascript" src="transport.js"></script>
+ <script type="text/javascript">
+ address = '127.0.0.1:8080';
+ conn = initTransport(function() {
+ // connected cb
+ log('Connected!');
+ },
+ function(data) {
+ // data cb
+ log('Received: ' + data);
+ }, function() {
+ // disconnected cb
+ log('Lost connection with the server');
+ }, function(msg) {
+ // error cb
+ log('Unexpected error while communicating with server: ' + msg);
+ });
+ if (!conn.connect)
+ alert('Sorry, you are using an unsupported browser.');
+
+ function log(text) {
+ document.getElementById('log').innerHTML += text + '<br />';
+ }
+ function send() {
+ var text = document.getElementById('text').value,
+ ret = conn.send(text);
+ if (typeof ret === 'string')
+ log('Error while sending data: ' + ret);
+ else
+ log('Sent: ' + text);
+ }
+ </script>
+ <style type="text/css">
+ input, a { margin-right: 15px; }
+ .input, .log { border: 1px solid black; }
+ .log { padding: 10px; margin-bottom: 30px; }
+ </style>
+ </head>
+ <body>
+ <form onsubmit="return false">
+ <input type="button" value="Connect" onclick="conn.connect(address); return false;" />
+ <input type="text" id="text" class="input">
+ <input type="button" value="Send" onclick="send(); return false;" />
+ <input type="button" value="Disconnect" onclick="conn.disconnect(); return false;" />
+ </form>
+ <div id="log" class="log"></div>
+ </body>
+</html>
View
29 examples/echo/flashws.js
@@ -0,0 +1,29 @@
+var swfobject=function(){function o(){if(!A){try{var a=h.getElementsByTagName("body")[0].appendChild(h.createElement("span"));a.parentNode.removeChild(a)}catch(b){return}A=true;a=G.length;for(var c=0;c<a;c++)G[c]()}}function d(a){if(A)a();else G[G.length]=a}function k(a){if(typeof q.addEventListener!=l)q.addEventListener("load",a,false);else if(typeof h.addEventListener!=l)h.addEventListener("load",a,false);else if(typeof q.attachEvent!=l)aa(q,"onload",a);else if(typeof q.onload=="function"){var b=
+q.onload;q.onload=function(){b();a()}}else q.onload=a}function p(){var a=h.getElementsByTagName("body")[0],b=h.createElement(u);b.setAttribute("type",H);var c=a.appendChild(b);if(c){var e=0;(function(){if(typeof c.GetVariable!=l){var f=c.GetVariable("$version");if(f){f=f.split(" ")[1].split(",");g.pv=[parseInt(f[0],10),parseInt(f[1],10),parseInt(f[2],10)]}}else if(e<10){e++;setTimeout(arguments.callee,10);return}a.removeChild(b);c=null;s()})()}else s()}function s(){var a=x.length;if(a>0)for(var b=
+0;b<a;b++){var c=x[b].id,e=x[b].callbackFn,f={success:false,id:c};if(g.pv[0]>0){var j=t(c);if(j)if(I(x[b].swfVersion)&&!(g.wk&&g.wk<312)){B(c,true);if(e){f.success=true;f.ref=D(c);e(f)}}else if(x[b].expressInstall&&r()){f={};f.data=x[b].expressInstall;f.width=j.getAttribute("width")||"0";f.height=j.getAttribute("height")||"0";if(j.getAttribute("class"))f.styleclass=j.getAttribute("class");if(j.getAttribute("align"))f.align=j.getAttribute("align");var i={};j=j.getElementsByTagName("param");for(var m=
+j.length,n=0;n<m;n++)if(j[n].getAttribute("name").toLowerCase()!="movie")i[j[n].getAttribute("name")]=j[n].getAttribute("value");P(f,i,c,e)}else{ba(j);e&&e(f)}}else{B(c,true);if(e){if((c=D(c))&&typeof c.SetVariable!=l){f.success=true;f.ref=c}e(f)}}}}function D(a){var b=null;if((a=t(a))&&a.nodeName=="OBJECT")if(typeof a.SetVariable!=l)b=a;else if(a=a.getElementsByTagName(u)[0])b=a;return b}function r(){return!J&&I("6.0.65")&&(g.win||g.mac)&&!(g.wk&&g.wk<312)}function P(a,b,c,e){J=true;Q=e||null;U=
+{success:false,id:c};var f=t(c);if(f){if(f.nodeName=="OBJECT"){E=R(f);K=null}else{E=f;K=c}a.id=V;if(typeof a.width==l||!/%$/.test(a.width)&&parseInt(a.width,10)<310)a.width="310";if(typeof a.height==l||!/%$/.test(a.height)&&parseInt(a.height,10)<137)a.height="137";h.title=h.title.slice(0,47)+" - Flash Player Installation";e=g.ie&&g.win?"ActiveX":"PlugIn";e="MMredirectURL="+q.location.toString().replace(/&/g,"%26")+"&MMplayerType="+e+"&MMdoctitle="+h.title;if(typeof b.flashvars!=l)b.flashvars+="&"+
+e;else b.flashvars=e;if(g.ie&&g.win&&f.readyState!=4){e=h.createElement("div");c+="SWFObjectNew";e.setAttribute("id",c);f.parentNode.insertBefore(e,f);f.style.display="none";(function(){f.readyState==4?f.parentNode.removeChild(f):setTimeout(arguments.callee,10)})()}S(a,b,c)}}function ba(a){if(g.ie&&g.win&&a.readyState!=4){var b=h.createElement("div");a.parentNode.insertBefore(b,a);b.parentNode.replaceChild(R(a),b);a.style.display="none";(function(){a.readyState==4?a.parentNode.removeChild(a):setTimeout(arguments.callee,
+10)})()}else a.parentNode.replaceChild(R(a),a)}function R(a){var b=h.createElement("div");if(g.win&&g.ie)b.innerHTML=a.innerHTML;else if(a=a.getElementsByTagName(u)[0])if(a=a.childNodes)for(var c=a.length,e=0;e<c;e++)!(a[e].nodeType==1&&a[e].nodeName=="PARAM")&&a[e].nodeType!=8&&b.appendChild(a[e].cloneNode(true));return b}function S(a,b,c){var e,f=t(c);if(g.wk&&g.wk<312)return e;if(f){if(typeof a.id==l)a.id=c;if(g.ie&&g.win){var j="",i;for(i in a)if(a[i]!=Object.prototype[i])if(i.toLowerCase()==
+"data")b.movie=a[i];else if(i.toLowerCase()=="styleclass")j+=' class="'+a[i]+'"';else if(i.toLowerCase()!="classid")j+=" "+i+'="'+a[i]+'"';i="";for(var m in b)if(b[m]!=Object.prototype[m])i+='<param name="'+m+'" value="'+b[m]+'" />';f.outerHTML='<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"'+j+">"+i+"</object>";L[L.length]=a.id;e=t(a.id)}else{m=h.createElement(u);m.setAttribute("type",H);for(var n in a)if(a[n]!=Object.prototype[n])if(n.toLowerCase()=="styleclass")m.setAttribute("class",
+a[n]);else n.toLowerCase()!="classid"&&m.setAttribute(n,a[n]);for(j in b)if(b[j]!=Object.prototype[j]&&j.toLowerCase()!="movie"){a=m;i=j;n=b[j];c=h.createElement("param");c.setAttribute("name",i);c.setAttribute("value",n);a.appendChild(c)}f.parentNode.replaceChild(m,f);e=m}}return e}function W(a){var b=t(a);if(b&&b.nodeName=="OBJECT")if(g.ie&&g.win){b.style.display="none";(function(){if(b.readyState==4){var c=t(a);if(c){for(var e in c)if(typeof c[e]=="function")c[e]=null;c.parentNode.removeChild(c)}}else setTimeout(arguments.callee,
+10)})()}else b.parentNode.removeChild(b)}function t(a){var b=null;try{b=h.getElementById(a)}catch(c){}return b}function aa(a,b,c){a.attachEvent(b,c);C[C.length]=[a,b,c]}function I(a){var b=g.pv;a=a.split(".");a[0]=parseInt(a[0],10);a[1]=parseInt(a[1],10)||0;a[2]=parseInt(a[2],10)||0;return b[0]>a[0]||b[0]==a[0]&&b[1]>a[1]||b[0]==a[0]&&b[1]==a[1]&&b[2]>=a[2]?true:false}function X(a,b,c,e){if(!(g.ie&&g.mac)){var f=h.getElementsByTagName("head")[0];if(f){c=c&&typeof c=="string"?c:"screen";if(e)T=v=null;
+if(!v||T!=c){e=h.createElement("style");e.setAttribute("type","text/css");e.setAttribute("media",c);v=f.appendChild(e);if(g.ie&&g.win&&typeof h.styleSheets!=l&&h.styleSheets.length>0)v=h.styleSheets[h.styleSheets.length-1];T=c}if(g.ie&&g.win)v&&typeof v.addRule==u&&v.addRule(a,b);else v&&typeof h.createTextNode!=l&&v.appendChild(h.createTextNode(a+" {"+b+"}"))}}}function B(a,b){if(Y){var c=b?"visible":"hidden";if(A&&t(a))t(a).style.visibility=c;else X("#"+a,"visibility:"+c)}}function Z(a){return/[\\\"<>\.;]/.exec(a)!=
+null&&typeof encodeURIComponent!=l?encodeURIComponent(a):a}var l="undefined",u="object",H="application/x-shockwave-flash",V="SWFObjectExprInst",q=window,h=document,y=navigator,$=false,G=[function(){$?p():s()}],x=[],L=[],C=[],E,K,Q,U,A=false,J=false,v,T,Y=true,g=function(){var a=typeof h.getElementById!=l&&typeof h.getElementsByTagName!=l&&typeof h.createElement!=l,b=y.userAgent.toLowerCase(),c=y.platform.toLowerCase(),e=c?/win/.test(c):/win/.test(b);c=c?/mac/.test(c):/mac/.test(b);b=/webkit/.test(b)?
+parseFloat(b.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false;var f=!+"\u000b1",j=[0,0,0],i=null;if(typeof y.plugins!=l&&typeof y.plugins["Shockwave Flash"]==u){if((i=y.plugins["Shockwave Flash"].description)&&!(typeof y.mimeTypes!=l&&y.mimeTypes[H]&&!y.mimeTypes[H].enabledPlugin)){$=true;f=false;i=i.replace(/^.*\s+(\S+\s+\S+$)/,"$1");j[0]=parseInt(i.replace(/^(.*)\..*$/,"$1"),10);j[1]=parseInt(i.replace(/^.*\.(.*)\s.*$/,"$1"),10);j[2]=/[a-zA-Z]/.test(i)?parseInt(i.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),
+10):0}}else if(typeof q.ActiveXObject!=l)try{var m=new ActiveXObject("ShockwaveFlash.ShockwaveFlash");if(m)if(i=m.GetVariable("$version")){f=true;i=i.split(" ")[1].split(",");j=[parseInt(i[0],10),parseInt(i[1],10),parseInt(i[2],10)]}}catch(n){}return{w3:a,pv:j,wk:b,ie:f,win:e,mac:c}}();(function(){if(g.w3){if(typeof h.readyState!=l&&h.readyState=="complete"||typeof h.readyState==l&&(h.getElementsByTagName("body")[0]||h.body))o();if(!A){typeof h.addEventListener!=l&&h.addEventListener("DOMContentLoaded",
+o,false);if(g.ie&&g.win){h.attachEvent("onreadystatechange",function(){if(h.readyState=="complete"){h.detachEvent("onreadystatechange",arguments.callee);o()}});q==top&&function(){if(!A){try{h.documentElement.doScroll("left")}catch(a){setTimeout(arguments.callee,0);return}o()}}()}g.wk&&function(){A||(/loaded|complete/.test(h.readyState)?o():setTimeout(arguments.callee,0))}();k(o)}}})();(function(){g.ie&&g.win&&window.attachEvent("onunload",function(){for(var a=C.length,b=0;b<a;b++)C[b][0].detachEvent(C[b][1],
+C[b][2]);a=L.length;for(b=0;b<a;b++)W(L[b]);for(var c in g)g[c]=null;g=null;for(var e in swfobject)swfobject[e]=null;swfobject=null})})();return{registerObject:function(a,b,c,e){if(g.w3&&a&&b){var f={};f.id=a;f.swfVersion=b;f.expressInstall=c;f.callbackFn=e;x[x.length]=f;B(a,false)}else e&&e({success:false,id:a})},getObjectById:function(a){if(g.w3)return D(a)},embedSWF:function(a,b,c,e,f,j,i,m,n,F){var M={success:false,id:b};if(g.w3&&!(g.wk&&g.wk<312)&&a&&b&&c&&e&&f){B(b,false);d(function(){c+="";
+e+="";var z={};if(n&&typeof n===u)for(var w in n)z[w]=n[w];z.data=a;z.width=c;z.height=e;w={};if(m&&typeof m===u)for(var N in m)w[N]=m[N];if(i&&typeof i===u)for(var O in i)if(typeof w.flashvars!=l)w.flashvars+="&"+O+"="+i[O];else w.flashvars=O+"="+i[O];if(I(f)){N=S(z,w,b);z.id==b&&B(b,true);M.success=true;M.ref=N}else if(j&&r()){z.data=j;P(z,w,b,F);return}else B(b,true);F&&F(M)})}else F&&F(M)},switchOffAutoHideShow:function(){Y=false},ua:g,getFlashPlayerVersion:function(){return{major:g.pv[0],minor:g.pv[1],
+release:g.pv[2]}},hasFlashPlayerVersion:I,createSWF:function(a,b,c){if(g.w3)return S(a,b,c)},showExpressInstall:function(a,b,c,e){g.w3&&r()&&P(a,b,c,e)},removeSWF:function(a){g.w3&&W(a)},createCSS:function(a,b,c,e){g.w3&&X(a,b,c,e)},addDomLoadEvent:d,addLoadEvent:k,getQueryParamValue:function(a){var b=h.location.search||h.location.hash;if(b){if(/\?/.test(b))b=b.split("?")[1];if(a==null)return Z(b);b=b.split("&");for(var c=0;c<b.length;c++)if(b[c].substring(0,b[c].indexOf("="))==a)return Z(b[c].substring(b[c].indexOf("=")+
+1))}return""},expressInstallCallback:function(){if(J){var a=t(V);if(a&&E){a.parentNode.replaceChild(E,a);if(K){B(K,true);if(g.ie&&g.win)E.style.display="block"}Q&&Q(U)}J=false}}}}();
+(function(){if(!window.WebSocket){var o=window.console;if(!o||!o.log||!o.error)o={log:function(){},error:function(){}};if(swfobject.hasFlashPlayerVersion("10.0.0")){location.protocol=="file:"&&o.error("WARNING: web-socket-js doesn't work in file:///... URL unless you set Flash Security Settings properly. Open the page via Web server i.e. http://...");WebSocket=function(d,k,p,s,D){var r=this;r.__id=WebSocket.__nextId++;WebSocket.__instances[r.__id]=r;r.readyState=WebSocket.CONNECTING;r.bufferedAmount=
+0;r.__events={};setTimeout(function(){WebSocket.__addTask(function(){WebSocket.__flash.create(r.__id,d,k,p||null,s||0,D||null)})},0)};WebSocket.prototype.send=function(d){if(this.readyState==WebSocket.CONNECTING)throw"INVALID_STATE_ERR: Web Socket connection has not been established";d=WebSocket.__flash.send(this.__id,encodeURIComponent(d));if(d<0)return true;else{this.bufferedAmount+=d;return false}};WebSocket.prototype.close=function(){if(!(this.readyState==WebSocket.CLOSED||this.readyState==WebSocket.CLOSING)){this.readyState=
+WebSocket.CLOSING;WebSocket.__flash.close(this.__id)}};WebSocket.prototype.addEventListener=function(d,k){d in this.__events||(this.__events[d]=[]);this.__events[d].push(k)};WebSocket.prototype.removeEventListener=function(d,k){if(d in this.__events)for(var p=this.__events[d],s=p.length-1;s>=0;--s)if(p[s]===k){p.splice(s,1);break}};WebSocket.prototype.dispatchEvent=function(d){for(var k=this.__events[d.type]||[],p=0;p<k.length;++p)k[p](d);(k=this["on"+d.type])&&k(d)};WebSocket.prototype.__handleEvent=
+function(d){if("readyState"in d)this.readyState=d.readyState;if(d.type=="open"||d.type=="error")d=this.__createSimpleEvent(d.type);else if(d.type=="close")d=this.__createSimpleEvent("close");else if(d.type=="message")d=this.__createMessageEvent("message",decodeURIComponent(d.message));else throw"unknown event type: "+d.type;this.dispatchEvent(d)};WebSocket.prototype.__createSimpleEvent=function(d){if(document.createEvent&&window.Event){var k=document.createEvent("Event");k.initEvent(d,false,false);
+return k}else return{type:d,bubbles:false,cancelable:false}};WebSocket.prototype.__createMessageEvent=function(d,k){if(document.createEvent&&window.MessageEvent&&!window.opera){var p=document.createEvent("MessageEvent");p.initMessageEvent("message",false,false,k,null,null,window,null);return p}else return{type:d,data:k,bubbles:false,cancelable:false}};WebSocket.CONNECTING=0;WebSocket.OPEN=1;WebSocket.CLOSING=2;WebSocket.CLOSED=3;WebSocket.__flash=null;WebSocket.__instances={};WebSocket.__tasks=[];
+WebSocket.__nextId=0;WebSocket.loadFlashPolicyFile=function(d){WebSocket.__addTask(function(){WebSocket.__flash.loadManualPolicyFile(d)})};WebSocket.__initialize=function(){if(!WebSocket.__flash){if(WebSocket.__swfLocation)window.WEB_SOCKET_SWF_LOCATION=WebSocket.__swfLocation;if(window.WEB_SOCKET_SWF_LOCATION){var d=document.createElement("div");d.id="webSocketContainer";d.style.position="absolute";if(WebSocket.__isFlashLite()){d.style.left="0px";d.style.top="0px"}else{d.style.left="-100px";d.style.top=
+"-100px"}var k=document.createElement("div");k.id="webSocketFlash";d.appendChild(k);document.body.appendChild(d);swfobject.embedSWF(WEB_SOCKET_SWF_LOCATION,"webSocketFlash","1","1","10.0.0",null,null,{hasPriority:true,swliveconnect:true,allowScriptAccess:"always"},null,function(p){p.success||o.error("[WebSocket] swfobject.embedSWF failed")})}else o.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf")}};WebSocket.__onFlashInitialized=function(){setTimeout(function(){WebSocket.__flash=
+document.getElementById("webSocketFlash");WebSocket.__flash.setCallerUrl(location.href);WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG);for(var d=0;d<WebSocket.__tasks.length;++d)WebSocket.__tasks[d]();WebSocket.__tasks=[]},0)};WebSocket.__onFlashEvent=function(){setTimeout(function(){try{for(var d=WebSocket.__flash.receiveEvents(),k=0;k<d.length;++k)WebSocket.__instances[d[k].webSocketId].__handleEvent(d[k])}catch(p){o.error(p)}},0);return true};WebSocket.__log=function(d){o.log(decodeURIComponent(d))};
+WebSocket.__error=function(d){o.error(decodeURIComponent(d))};WebSocket.__addTask=function(d){WebSocket.__flash?d():WebSocket.__tasks.push(d)};WebSocket.__isFlashLite=function(){if(!window.navigator||!window.navigator.mimeTypes)return false;var d=window.navigator.mimeTypes["application/x-shockwave-flash"];if(!d||!d.enabledPlugin||!d.enabledPlugin.filename)return false;return d.enabledPlugin.filename.match(/flashlite/i)?true:false};window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION||(window.addEventListener?
+window.addEventListener("load",function(){WebSocket.__initialize()},false):window.attachEvent("onload",function(){WebSocket.__initialize()}))}else o.error("Flash Player >= 10.0.0 is required.")}})();
View
112 examples/echo/server.js
@@ -0,0 +1,112 @@
+var fs = require('fs'),
+ grappler = require('../../lib/grappler'),
+ common = require('../../lib/common');
+
+var localFiles = {},
+ address = "",
+ port = 8080;
+
+// Load the demo page
+try {
+ localFiles['demo.htm'] = fs.readFileSync('demo.htm');
+} catch (err) {
+ console.log('An error occurred while reading \'demo.htm\': ' + err);
+ process.exit(1);
+}
+
+// Load the minified javascript needed to add support for Flash WebSockets
+try {
+ localFiles['flashws.js'] = fs.readFileSync('flashws.js');
+} catch (err) {
+ console.log('An error occurred while reading \'flashws.js\': ' + err);
+ process.exit(1);
+}
+
+// Load the javascript client helper
+try {
+ localFiles['transport.js'] = fs.readFileSync('transport.js');
+} catch (err) {
+ console.log('An error occurred while reading \'transport.js\': ' + err);
+ process.exit(1);
+}
+
+// Load the Flash WebSocket file itself
+try {
+ localFiles['WebSocketMain.swf'] = fs.readFileSync('WebSocketMain.swf');
+} catch (err) {
+ console.log('An error occurred while reading \'WebSocketMain.swf\': ' + err);
+ process.exit(1);
+}
+
+// Create a new instance of a grappler server
+var echoServer = new grappler.Server({
+ logger: function(msg, level) {
+ if (level == common.LOG.INFO)
+ msg = 'INFO: ' + msg;
+ else if (level == common.LOG.WARN)
+ msg = 'WARN: ' + msg;
+ else
+ msg = 'ERROR: ' + msg;
+ console.error('DEBUG: ' + msg);
+ }
+}, function(req, res) { // HTTP override function that lets us decide to handle requests instead of grappler
+ // We don't care to filter WebSocket connections (e.g. check validity of cookies, etc)
+ if (req.headers.upgrade)
+ return;
+
+ var file = req.url.substr(1), type = 'application/octet-stream';
+ if (localFiles[file]) {
+ switch (file.substr(file.lastIndexOf('.')+1)) {
+ case 'js':
+ type = 'text/javascript';
+ break;
+ case 'swf':
+ type = 'application/x-shockwave-flash';
+ break;
+ case 'htm':
+ case 'html':
+ type = 'text/html';
+ break;
+ }
+ res.writeHead(200, {
+ 'Connection': 'close',
+ 'Content-Type': type,
+ 'Content-Length': localFiles[file].length
+ });
+ res.end(localFiles[file]);
+ } else if (file.length) {
+ res.writeHead(404, { 'Connection': 'close' });
+ res.end();
+ }
+});
+
+// Listen for an incoming connection
+echoServer.on('connection', function(client) {
+ var type;
+ if (client.state & common.STATE.PROTO_WEBSOCKET)
+ type = "WebSocket";
+ else if (client.state & common.STATE.PROTO_HTTP)
+ type = "HTTP";
+ else
+ type = "Unknown";
+
+ console.log(type + ' client connected from ' + client.remoteAddress);
+
+ client.on('message', function(msg) {
+ var text = '';
+ msg.on('data', function(data) {
+ text += data;
+ });
+ msg.on('end', function() {
+ console.log('Received the following message from a client @ ' + client.remoteAddress + ': ' + text);
+ //client.write(text);
+ echoServer.broadcast(text);
+ });
+ });
+ client.on('end', function() {
+ console.log(type + ' client disconnected from ' + client.remoteAddress);
+ });
+});
+
+echoServer.listen(port);
+console.log('Echo server started on port ' + port);
View
227 examples/echo/transport.js
@@ -0,0 +1,227 @@
+if (!String.prototype.trim) {
+ String.prototype.trim = function() {
+ var str = this.replace(/^\s\s*/, ''),
+ ws = /\s/,
+ i = str.length;
+ while (ws.test(str.charAt(--i)));
+ return str.slice(0, i + 1);
+ }
+}
+
+function initTransport(cbConnect, cbData, cbDisconnect, cbError) {
+ var transport = {
+ connect: null,
+ send: null,
+ disconnect: null,
+ _instance: null,
+ _host: null
+ };
+ cbConnect = cbConnect || empty;
+ cbData = cbData || empty;
+ cbDisconnect = cbDisconnect || empty;
+ cbError = cbError || empty;
+
+ WEB_SOCKET_SWF_LOCATION = "WebSocketMain.swf";
+ WEB_SOCKET_DEBUG = false;
+
+ function getXHR() {
+ var xhr = null;
+ try {
+ xhr = new XMLHttpRequest();
+ } catch(e) {}
+ if (!xhr) {
+ try {
+ xhr = new ActiveXObject('Msxml2.XMLHTTP.6.0');
+ } catch(e) {}
+ }
+ if (!xhr) {
+ try {
+ xhr = new ActiveXObject('Msxml2.XMLHTTP.3.0');
+ } catch(e) {}
+ }
+ if (!xhr) {
+ try {
+ xhr = new ActiveXObject('Msxml2.XMLHTTP');
+ } catch(e) {}
+ }
+ return xhr;
+ };
+
+ function xhrSendData(host, data, method, callback) {
+ var xhr = getXHR();
+ if (typeof method === 'function') {
+ callback = method;
+ method = undefined;
+ }
+ callback = callback || empty;
+
+ xhr.open(method || 'POST', 'http://' + host);
+ /*xhr.onreadystatechange = function() {
+ if (xhr.readyStatus == 4) {
+ if (xhr.status == 0)
+ callback('Error while sending data: Unable to connect');
+ else if (xhr.status == 200)
+ callback();
+ else
+ callback('Error while sending data: Unexpected HTTP status code: ' + xhr.status);
+ }
+ };*/
+ if (data)
+ xhr.send(data);
+ else
+ xhr.send();
+
+ if (xhr.status == 0)
+ return 'Error while sending data: Unable to connect';
+ else if (xhr.status == 200)
+ return true;
+ else
+ return 'Error while sending data: Unexpected HTTP status code: ' + xhr.status;
+ }
+
+ if (window.WebSocket) {
+ transport.connect = function(host) {
+ transport._host = host;
+ transport._instance = new WebSocket("ws://" + host);
+ transport._instance.onmessage = function(ev) { cbData(ev.data); };
+ transport._instance.onclose = function() { cbDisconnect(); };
+ transport._instance.onopen = function() { cbConnect(); };
+ transport._instance.onerror = function() { if (transport._instance.readyState == 3) cbError('Unable to connect'); };
+ }
+ transport.disconnect = function() {
+ try {
+ transport._instance.close();
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+ transport.send = function(data) {
+ try {
+ return (data.length > 0 ? transport._instance.send(data) : 'Nothing to send')
+ } catch (e) {
+ return ''+e;
+ }
+ }
+ } else if (typeof getXHR().multipart !== 'undefined') {
+ transport.connect = function(host) {
+ transport._host = host;
+ var fnTimeout = function() { if (!transport._instance.dcManual) transport.disconnect(); };
+ var timeoutVal = 4000;
+ transport._instance = (transport._instance ? transport._instance : getXHR());
+ transport._instance.multipart = true;
+ transport._instance.open('GET', 'http://' + host + '/?' + (new Date()).getTime(), true);
+ transport._instance.isFirst = true;
+ transport._instance.dcManual = false;
+ transport._instance.setRequestHeader('Accept', 'multipart/x-mixed-replace');
+ transport._instance.onreadystatechange = function() {
+ if (transport._instance.isFirst) {
+ transport._instance.isFirst = false;
+ cbConnect();
+ }
+ if (transport._instance.readyState == 3)
+ cbData(transport._instance.responseText);
+ else if (transport._instance.readyState == 4)
+ if (transport._instance.status == 0)
+ cbError('Unable to connect');
+ else {
+ if (transport._instance.dcTimeout)
+ clearTimeout(transport._instance.dcTimeout);
+ transport._instance.dcTimeout = setTimeout(fnTimeout, timeoutVal);
+ }
+ };
+ transport._instance.send();
+ }
+ transport.disconnect = function() {
+ try {
+ transport._instance.dcManual = true;
+ transport._instance.abort();
+ cbDisconnect();
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+ transport.send = function(data) {
+ try {
+ return (data.length > 0 ? xhrSendData(transport._host, data) : 'Nothing to send');
+ } catch (e) {
+ return 'Unable to connect';
+ }
+ }
+ } else if (window.EventSource) {
+ transport.connect = function(host) {
+ transport._host = host;
+ transport._instance.dcManual = false;
+ transport._instance = new EventSource('http://' + host + '/?' + (new Date()).getTime());
+ transport._instance.onmessage = function(ev) { cbData(ev.data); };
+ transport._instance.onopen = function(ev) { cbConnect(); };
+ transport._instance.onerror = function(ev) {
+ if (transport._instance.readyState != 2 && transport._instance.dcManual)
+ cbDisconnect();
+ else if (transport._instance.readyState == 2 && !transport._instance.dcManual)
+ cbError('Unable to connect');
+ };
+ }
+ transport.disconnect = function() {
+ try {
+ transport._instance.dcManual = true;
+ transport._instance.close();
+ // It seems onerror() isn't fired when using .close()?
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+ transport.send = function(data) {
+ try {
+ return (data.length > 0 ? xhrSendData(transport._host, data) : 'Nothing to send');
+ } catch (e) {
+ return 'Unable to connect';
+ }
+ }
+ } else if (getXHR()) {
+ transport.connect = function(host) {
+ if (arguments.length == 1) {
+ transport._host = host;
+ transport._instance = getXHR();
+ transport._instance.dcManual = false;
+ transport._instance.onreadystatechange = function() {
+ if (transport._instance.readyState == 4) {
+ if (transport._instance.status == 200) {
+ if (transport._instance.responseText.length)
+ cbData(transport._instance.responseText);
+ if (!transport._instance.dcManual)
+ setTimeout(function() { transport.connect(); }, 1);
+ } else if (transport._instance.status == 0) {
+ cbDisconnect();
+ transport.disconnect();
+ }
+ }
+ };
+ }
+ transport._instance.open('GET', 'http://' + transport._host + '/?' + (new Date()).getTime(), true);
+ transport._instance.send();
+ if (arguments.length == 1)
+ cbConnect();
+ }
+ transport.disconnect = function() {
+ try {
+ transport._instance.dcManual = true;
+ transport._instance.abort();
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+ transport.send = function(data) {
+ try {
+ return (data.length > 0 ? xhrSendData(transport._host, data) : 'Nothing to send');
+ } catch (e) {
+ return 'Unable to connect';
+ }
+ }
+ }
+
+ return transport;
+}
View
81 lib/common.js
@@ -0,0 +1,81 @@
+var util = require('util'),
+ EventEmitter = require('events').EventEmitter;
+
+exports.LOG = {
+ INFO: 1,
+ WARN: 2,
+ ERROR: 3
+};
+
+exports.STATE = {
+ ACCEPTED: 1,
+ TEMP: 2,
+ PROTO_HTTP: 4,
+ PROTO_WEBSOCKET: 8
+};
+
+ /**
+ * Adopted from jquery's extend method. Under the terms of MIT License.
+ *
+ * http://code.jquery.com/jquery-1.4.2.js
+ *
+ * Modified by Brian White to use Array.isArray instead of the custom isArray method
+ */
+exports.extend = function extend() {
+ // copy reference to target object
+ var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy;
+ // Handle a deep copy situation
+ if (typeof target === "boolean") {
+ deep = target;
+ target = arguments[1] || {};
+ // skip the boolean and the target
+ i = 2;
+ }
+ // Handle case when target is a string or something (possible in deep copy)
+ if (typeof target !== "object" && !typeof target === 'function')
+ target = {};
+ var isPlainObject = function(obj) {
+ // Must be an Object.
+ // Because of IE, we also have to check the presence of the constructor property.
+ // Make sure that DOM nodes and window objects don't pass through, as well
+ if (!obj || toString.call(obj) !== "[object Object]" || obj.nodeType || obj.setInterval)
+ return false;
+ var has_own_constructor = hasOwnProperty.call(obj, "constructor");
+ var has_is_property_of_method = hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf");
+ // Not own constructor property must be Object
+ if (obj.constructor && !has_own_constructor && !has_is_property_of_method)
+ return false;
+ // Own properties are enumerated firstly, so to speed up,
+ // if last one is own, then all properties are own.
+ var last_key;
+ for (key in obj)
+ last_key = key;
+ return typeof last_key === "undefined" || hasOwnProperty.call(obj, last_key);
+ };
+ for (; i < length; i++) {
+ // Only deal with non-null/undefined values
+ if ((options = arguments[i]) !== null) {
+ // Extend the base object
+ for (name in options) {
+ src = target[name];
+ copy = options[name];
+ // Prevent never-ending loop
+ if (target === copy)
+ continue;
+ // Recurse if we're merging object literal values or arrays
+ if (deep && copy && (isPlainObject(copy) || Array.isArray(copy))) {
+ var clone = src && (isPlainObject(src) || Array.isArray(src)) ? src : Array.isArray(copy) ? [] : {};
+ // Never move original objects, clone them
+ target[name] = extend(deep, clone, copy);
+ // Don't bring in undefined values
+ } else if (typeof copy !== "undefined")
+ target[name] = copy;
+ }
+ }
+ }
+ // Return the modified object
+ return target;
+};
+
+var Message = exports.Message = function() {};
+util.inherits(Message, EventEmitter);
View
355 lib/grappler.js
@@ -1,114 +1,32 @@
require.paths.unshift(__dirname);
-var sys = require('sys'),
- http = require('http'),
- fs = require('fs'),
- EventEmitter = require('events').EventEmitter,
- TcpClient = require('tcp.client').TcpClient,
- HttpClient;
+var util = require('util'),
+ http = require('http'),
+ fs = require('fs'),
+ EventEmitter = require('events').EventEmitter,
+ common = require('common'),
+ HttpClient;
// Dynamically generate the secret for securing HTTP cookies only once.
// We could keep from having to read in http.client.js twice every time
// if we just had some method to force require() to reload a single module
// from disk (only if HttpClient.cookieSecret == null).
-try {
- var clientCode = fs.readFileSync(__dirname + '/http.client.js').toString();
- if (clientCode.indexOf(".secret = null") > -1)
- fs.writeFileSync(__dirname + '/http.client.js', clientCode.replace(".secret = null", ".secret = '" + Math.floor(Math.random()*1e9).toString() + (new Date()).getTime().toString() + Math.floor(Math.random()*1e9).toString() + "'"));
- HttpClient = require('http.client').HttpClient;
-} catch (e) {
- throw e;
-}
-
-var LOG = exports.LOG = {
- INFO: 1,
- WARN: 2,
- ERROR: 3
-};
-
-var STATE = exports.STATE = {
- ACCEPTED: 1,
- TEMP: 2,
- PROTO_HTTP: 4,
- PROTO_WEBSOCKET: 8,
- PROTO_TCP: 16
-};
-
-// From jQuery.extend in the jQuery JavaScript Library v1.3.2
-// Copyright (c) 2009 John Resig
-// Dual licensed under the MIT and GPL licenses.
-// http://docs.jquery.com/License
-// Modified for node.js (formerly process.mixin)
-var mixin = exports.mixin = function() {
- // copy reference to target object
- var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, source;
-
- // Handle a deep copy situation
- if ( typeof target === "boolean" ) {
- deep = target;
- target = arguments[1] || {};
- // skip the boolean and the target
- i = 2;
- }
-
- // Handle case when target is a string or something (possible in deep copy)
- if ( typeof target !== "object" && !(typeof target === 'function') )
- target = {};
-
- // mixin process itself if only one argument is passed
- if ( length == i ) {
- target = GLOBAL;
- --i;
- }
-
- for ( ; i < length; i++ ) {
- // Only deal with non-null/undefined values
- if ( (source = arguments[i]) != null ) {
- // Extend the base object
- //Object.getOwnPropertyNames(source).forEach(function(k){
- for (var j=0, k, keys=Object.getOwnPropertyNames(source), len=keys.length; j<len; ++j) {
- k = keys[j];
- var d = Object.getOwnPropertyDescriptor(source, k) || {value: source[k]};
- if (d.get) {
- target.__defineGetter__(k, d.get);
- if (d.set) {
- target.__defineSetter__(k, d.set);
- }
- }
- else {
- // Prevent never-ending loop
- if (target === d.value) {
- return;
- }
-
- if (deep && d.value && typeof d.value === "object") {
- target[k] = mixin(deep,
- // Never move original objects, clone them
- source[k] || (d.value.length != null ? [] : {})
- , d.value);
- }
- else {
- target[k] = d.value;
- }
- }
- }//);
- }
- }
- // Return the modified object
- return target;
-};
+var clientCode = fs.readFileSync(__dirname + '/http.client.js').toString();
+if (clientCode.indexOf('.secret = null') > -1)
+ fs.writeFileSync(__dirname + '/http.client.js', clientCode.replace('.secret = null', ".secret = '" + Math.floor(Math.random()*1e9).toString() + (new Date()).getTime().toString() + Math.floor(Math.random()*1e9).toString() + "'"));
+HttpClient = require('http.client');
function noop() { return true; };
function noaction() { return false; };
function parseOrigins(allowedOrigins) {
- if (Array.isArray(allowedOrigins) && arguments.length == 1)
- return allowedOrigins.reduce(parseOrigins, []);
- else {
- var allowedHost = (arguments[1] ? arguments[1].split(":") : allowedOrigins.split(":")),
- allowedPort = (allowedHost.length == 2 ? allowedHost[1] : '*');
- allowedHost = allowedHost[0];
- return (arguments[1] ? allowedOrigins.concat([[allowedHost, allowedPort]]) : [[allowedHost, allowedPort]]);
- }
+ if (Array.isArray(allowedOrigins) && arguments.length === 1)
+ return allowedOrigins.reduce(parseOrigins, []);
+ else {
+ var allowedHost = (arguments[1] ? arguments[1].split(':') : allowedOrigins.split(':')),
+ allowedPort = (allowedHost.length === 2 ? allowedHost[1] : '*');
+ allowedHost = allowedHost[0];
+ return (arguments[1] ? allowedOrigins.concat([[allowedHost, allowedPort]]) : [[allowedHost, allowedPort]]);
+ }
}
// When calling the Server() constructor, you must either give 1 or 3 arguments. Specify null for any callbacks you
@@ -121,144 +39,133 @@ function parseOrigins(allowedOrigins) {
// fnAcceptClient(stream) is a callback that determines if a given net.Stream is permitted to stay connected to the server,
// judging by the callback's return value. If a callback is not supplied, the default action is to accept all clients.
function Server(options/*, fnHandleNormalHTTP, fnAcceptClient*/) {
- EventEmitter.call(this);
-
- var logger;
- var cbAccept = (arguments[2] && typeof arguments[2] == 'function' ? arguments[2] : noop);
- var cbHandleHTTP = (arguments[1] && typeof arguments[1] == 'function' ? arguments[1] : noaction);
- var server;
- var connections = this.connections = {};
- var self = this;
-
- this.options = mixin({
- logger: noop, // function that receives debug messages of various kinds and passes in two arguments: the message and the
- // debug level of the message (denoted by the LOG.* "constants")
- origins: "*:*", // which clients are allowed to connect? the port portion is only pertinent for HTTP clients
- pingInterval: 3000, // time in ms to ping the client for HTTP connections that need to do so
- detectTimeout: 0 // time in ms to wait for an HTTP response before assuming a plain TCP client. 0 disables this feature.
- }, options || {});
-
- this.options.origins = parseOrigins(this.options.origins);
- logger = this.options.logger;
- server = this._server = http.createServer();
-
- // Make sure our connection handler happens before the built-in one
- server.listeners('connection').unshift(function(socket) {
- var flashSocketTest = "";
- socket.addListener('data', function(buffer) {
- flashSocketTest += buffer.toString();
- if (flashSocketTest.indexOf("<policy-file-request/>") == 0) {
- var allowedOrigins = self.options.origins.reduce(function(prev, cur) {
- return prev + '<allow-access-from domain="' + cur[0] + '" to-ports="' + cur[1] + '"/>';
- }, "");
- socket.end('<?xml version="1.0"?><!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd"><cross-domain-policy>' + allowedOrigins + '</cross-domain-policy>');
- socket.destroy();
- }
- });
- // Override http.Server's built-in socket timeout
- socket.setTimeout(0);
- if (self.options.detectTimeout > 0) {
- socket.removeAllListeners('timeout');
- socket.addListener('timeout', function() {
- // Assume a non-HTTP client if we haven't received a valid HTTP request
- // in the time determined by options.detectTimeout
- if (!(socket.client.state & STATE.PROTO_HTTP)) {
- socket.client.state |= STATE.PROTO_TCP;
- connections[socket.client._id] = new TcpClient(socket.client);
- }
- });
- socket.setTimeout(self.options.detectTimeout);
- }
-
- socket.client = new Client(self, socket.remoteAddress);
-
- var fnClose = function() {
- if (socket.isMarked == undefined) {
- if (connections[socket.client._id])
- connections[socket.client._id].disconnect();
- socket.isMarked = true;
- logger('Server :: Connection closed: id == ' + socket.client._id, LOG.INFO);
- }
- };
- socket.addListener('close', fnClose);
- socket.addListener('end', fnClose);
-
- if (!cbAccept(socket)) {
- // The incoming connection was denied for one reason or another
- socket.destroy();
- logger('Server :: Incoming connection denied: id == ' + socket.client._id, LOG.INFO);
- } else {
- socket.client.state |= STATE.ACCEPTED;
- logger('Server :: Incoming connection accepted: id == ' + socket.client._id, LOG.INFO);
- }
- });
- server.addListener('request', function(req, res) {
- // Check if we have accepted the connection and have decided that this
- // is not a non-HTTP request
- if (req.connection.client.state & STATE.ACCEPTED) {
- req.connection.client.state |= STATE.PROTO_HTTP;
- req.connection.setTimeout(0);
- req.connection.removeAllListeners('timeout');
- req.connection.removeAllListeners('data');
-
- cbHandleHTTP(req, res);
+ EventEmitter.call(this);
+
+ var logger;
+ var cbAccept = (arguments[2] && typeof arguments[2] === 'function' ? arguments[2] : noop);
+ var cbHandleHTTP = (arguments[1] && typeof arguments[1] === 'function' ? arguments[1] : noaction);
+ var server;
+ var self = this;
+
+ this.options = common.extend({
+ logger: noop, // function that receives debug messages of various kinds and passes in two arguments: the message and the
+ // debug level of the message (denoted by the common.LOG.* "constants")
+ origins: '*:*', // which clients are allowed to connect? the port portion is only pertinent for HTTP clients
+ pingInterval: 3000, // time in ms to ping the client for HTTP connections that need to do so
+ storage: 'object' // use B+ Tree structure by default
+ }, options || {});
+
+ var Storage = require('./storage/storage.' + this.options.storage);
+ var connections = this.connections = new Storage();
+
+ this.options.origins = parseOrigins(this.options.origins);
+ logger = this.options.logger;
+ server = this._server = http.createServer();
+
+ // Make sure our connection handler happens before the built-in one
+ server.listeners('connection').unshift(function(socket) {
+ var flashSocketTest = '';
+ socket.on('data', function(chunk) {
+ flashSocketTest += chunk;
+ if (flashSocketTest.indexOf('<policy-file-request/>') === 0) {
+ var allowedOrigins = self.options.origins.reduce(function(prev, cur) {
+ return prev + '<allow-access-from domain="' + cur[0] + '" to-ports="' + cur[1] + '"/>';
+ }, '');
+ socket.end('<?xml version="1.0"?>\
+ <!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">\
+ <cross-domain-policy>' + allowedOrigins + '</cross-domain-policy>');
+ socket.destroy();
+ }
+ });
+ // Override http.Server's built-in socket timeout
+ socket.setTimeout(0);
+
+ socket.client = new Client(self, socket.remoteAddress);
+
+ var fnClose = function() {
+ if (typeof socket.isMarked === 'undefined') {
+ var user = connections.get(socket.client._id);
+ if (user)
+ user.disconnect();
+ socket.isMarked = true;
+ logger('Server :: Connection closed: id == ' + socket.client._id, common.LOG.INFO);
+ }
+ };
+ socket.on('close', fnClose);
+ socket.on('end', fnClose);
+
+ if (!cbAccept(socket)) {
+ // The incoming connection was denied for one reason or another
+ socket.destroy();
+ logger('Server :: Incoming connection denied: id == ' + socket.client._id, common.LOG.INFO);
+ } else {
+ socket.client.state |= common.STATE.ACCEPTED;
+ logger('Server :: Incoming connection accepted: id == ' + socket.client._id, common.LOG.INFO);
+ }
+ });
+
+ server.on('request', function(req, res) {
+ // Check if we have accepted the connection and have decided that this
+ // is not a non-HTTP request
+ if (req.connection.client.state & common.STATE.ACCEPTED) {
+ req.connection.client.state |= common.STATE.PROTO_HTTP;
+ req.connection.setTimeout(0);
+ req.connection.removeAllListeners('timeout');
+ req.connection.removeAllListeners('data');
+
+ cbHandleHTTP(req, res);
+
+ // Let grappler handle this request if it wasn't already handled by the callback
+ if (!res._header)
+ new HttpClient(req, res);
+ else
+ logger('Server :: HTTP connection handled by callback. id == ' + req.connection.client._id, common.LOG.INFO);
+ }
+ });
- // Let grappler handle this request if it wasn't already handled by the callback
- if (!res._header)
- new HttpClient(req, res);
- else
- logger('Server :: HTTP connection handled by callback. id == ' + req.connection.client._id, LOG.INFO);
- }
- });
- server.addListener('upgrade', function(req, socket, head) {
- if (req.connection.client.state & STATE.ACCEPTED) {
- req.connection.setTimeout(0);
- req.connection.removeAllListeners('timeout');
- req.connection.removeAllListeners('data');
+ server.on('upgrade', function(req, socket, head) {
+ if (req.connection.client.state & common.STATE.ACCEPTED) {
+ req.connection.setTimeout(0);
+ req.connection.removeAllListeners('timeout');
+ req.connection.removeAllListeners('data');
- cbHandleHTTP(req, socket);
+ cbHandleHTTP(req, socket);
- // Let grappler handle this request if it wasn't already handled by the callback
- if (req.connection.readyState == 'open')
- new HttpClient(req, socket, head);
- else
- logger('Server :: HTTP Upgrade request handled by callback. id == ' + req.connection.client._id, LOG.INFO);
- }
- });
+ // Let grappler handle this request if it wasn't already handled by the callback
+ if (req.connection.readyState === 'open')
+ new HttpClient(req, socket, head);
+ else
+ logger('Server :: HTTP Upgrade request handled by callback. id == ' + req.connection.client._id, common.LOG.INFO);
+ }
+ });
- server.addListener('error', function(err) {
- self.emit('error', err);
- });
+ server.on('error', function(err) {
+ self.emit('error', err);
+ });
- this.shutdown = function() {
- server.close();
- for (var i=0,keys=Object.keys(connections),len=keys.length; i<len; ++i)
- connections[keys[i]].disconnect();
- };
+ this.shutdown = function() {
+ server.close();
+ connections.do(function(key, user) { user.disconnect(); });
+ };
}
-sys.inherits(Server, EventEmitter);
+util.inherits(Server, EventEmitter);
exports.Server = Server;
Server.prototype.listen = function(port, host) {
- this._server.listen(port, host);
+ this._server.listen(port, host);
};
Server.prototype.broadcast = function(data, except) {
- for (var i=0,keys=Object.keys(this.connections),len=keys.length; i<len; ++i)
- if (!except || (keys[i] != except && this.connections[keys[i]] != except))
- this.connections[keys[i]].write(data);
+ this.connections.do(function(key, user) {
+ if (!except || (key !== except && user !== except))
+ user.write(data);
+ });
};
function Client(srv, ip) {
- this._id = Math.floor(Math.random()*1e5).toString() + (new Date()).getTime().toString();
- this.state = STATE.TEMP;
- this.remoteAddress = ip;
- var server = srv;
-
- this.__defineGetter__('server', function() { return server; });
-
- this.broadcast = function(data) {
- server.broadcast(data, this._id);
- };
+ this._id = Math.floor(Math.random()*1e5).toString() + (new Date()).getTime().toString();
+ this.state = common.STATE.TEMP;
+ this.remoteAddress = ip;
+ this.server = srv;
}
exports.Client = Client;
View
801 lib/http.client.js
@@ -1,431 +1,426 @@
require.paths.unshift(__dirname);
var url = require('url'),
- crypto = require('crypto'),
- Buffer = require('buffer').Buffer,
- EventEmitter = require('events').EventEmitter,
- sys = require('sys'),
- http = require('http'),
- grappler = require('grappler');
+ crypto = require('crypto'),
+ EventEmitter = require('events').EventEmitter,
+ util = require('util'),
+ http = require('http'),
+ common = require('common'),
+ Message = common.Message;
require('../deps/cookie-node').secret = null;
function isAllowed(allowedOrigins, testOrigin) {
- return allowedOrigins.some(function(origin) {
- var originParts = url.parse(testOrigin);
- originParts.port = originParts.port || 80;
- return ( (origin[0] == '*' || origin[0] == originParts.hostname) &&
- (origin[1] == '*' || origin[1] == originParts.port) );
- });
+ return allowedOrigins.some(function(origin) {
+ var originParts = url.parse(testOrigin);
+ originParts.port = originParts.port || 80;
+ return ( (origin[0] === '*' || origin[0] === originParts.hostname) &&
+ (origin[1] === '*' || origin[1] == originParts.port) );
+ });
}
function pack(num) {
- var result = '';
- result += String.fromCharCode(num >> 24 & 0xFF);
- result += String.fromCharCode(num >> 16 & 0xFF);
- result += String.fromCharCode(num >> 8 & 0xFF);
- result += String.fromCharCode(num & 0xFF);
- return result;
+ var result = '';
+ result += String.fromCharCode(num >> 24 & 0xFF);
+ result += String.fromCharCode(num >> 16 & 0xFF);
+ result += String.fromCharCode(num >> 8 & 0xFF);
+ result += String.fromCharCode(num & 0xFF);
+ return result;
}
-var HttpClient = function(req, res) {
- // Instance inheritance from the Client object :-\
- grappler.mixin(this, req.connection.client);
-
- var self = this;
- var server = self.server;
- this._request = req;
- this._response = res;
-
- // Ignore favicon requests since they weren't handled by the user's callback
- if (req.url == "/favicon.ico") {
- res.writeHead(404);
- res.end();
- return;
- }
-
- // Check for a permitted origin
- if (req.headers.origin && !isAllowed(server.options.origins, req.headers.origin)) {
- server.options.logger('HttpClient :: Denied client ' + self._id + ' due to disallowed origin (\'' + req.headers.origin + '\')', grappler.LOG.INFO);
- if (res instanceof http.ServerResponse) {
- res.writeHead(403);
- res.end();
- }
- req.connection.destroy();
- return;
- }
-
- if (req.headers.upgrade) {
- if (req.headers.upgrade == 'WebSocket')
- self.state |= grappler.STATE.PROTO_WEBSOCKET;
- else {
- self.disconnect();
- server.options.logger('HttpClient :: Unrecognized HTTP upgrade request: ' + req.headers.upgrade, grappler.LOG.WARN);
- }
- }
-
- // Check for a WebSocket request
- if (self.state & grappler.STATE.PROTO_WEBSOCKET) {
- self._makePerm();
- var draft = (typeof req.headers['sec-websocket-key1'] != 'undefined' &&
- typeof req.headers['sec-websocket-key2'] != 'undefined' ? 76 : 75),
- outgoingdata = ['HTTP/1.1 101 Web' + (draft == 75 ? ' ' : '') + 'Socket Protocol Handshake',
- 'Upgrade: WebSocket',
- 'Connection: Upgrade'],
- inBuffer = null;
-
- server.options.logger('HttpClient :: Using WebSocket draft ' + draft + ' for client id ' + self._id, grappler.LOG.INFO);
-
- if (draft == 75) {
- outgoingdata = outgoingdata.concat(['WebSocket-Origin: ' + req.headers.origin, 'WebSocket-Location: ws://' + req.headers.host + req.url]);
- outgoingdata = outgoingdata.concat(['', '']).join('\r\n');
- } else if (draft == 76) {
- var strkey1 = req.headers['sec-websocket-key1'],
- strkey2 = req.headers['sec-websocket-key2'],
- key1 = parseInt(strkey1.replace(/[^\d]/g, ""), 10),
- key2 = parseInt(strkey2.replace(/[^\d]/g, ""), 10),
- spaces1 = strkey1.replace(/[^\ ]/g, "").length,
- spaces2 = strkey2.replace(/[^\ ]/g, "").length;
-
- if (spaces1 == 0 || spaces2 == 0 || key1 % spaces1 != 0 || key2 % spaces2 != 0 && arguments[2].length == 8) {
- server.options.logger('HttpClient :: WebSocket request contained an invalid key. Closing connection.', grappler.LOG.WARN);
- self.disconnect();
- return;
- }
-
- outgoingdata = outgoingdata.concat(['Sec-WebSocket-Origin: ' + req.headers.origin, 'Sec-WebSocket-Location: ws://' + req.headers.host + req.url]);
- if (req.headers['Sec-WebSocket-Protocol'])
- outgoingdata = outgoingdata.concat(['Sec-WebSocket-Protocol: ' + req.headers['Sec-WebSocket-Protocol']]);
-
- var hash = crypto.createHash('md5');
- hash.update(pack(parseInt(key1/spaces1)));
- hash.update(pack(parseInt(key2/spaces2)));
- hash.update(arguments[2].toString('binary'));
- outgoingdata = outgoingdata.concat(['', '']).join('\r\n') + hash.digest('binary');
- }
- self._makePerm();
-
- req.connection.setNoDelay(true);
- req.connection.setKeepAlive(true, server.options.pingInterval);
- req.connection.write(outgoingdata, (draft == 75 ? 'ascii' : 'binary'));
- self.handshake = true;
-
- if (req.connection.readyState == 'open')
- server.emit('connection', this);
-
- req.connection.addListener('data', function(data) {
- var beginMarker = 0, endMarker = 255, curIdx, tmp;
- if (!inBuffer || inBuffer.length == 0) {
- inBuffer = new Buffer(data.length);
- data.copy(inBuffer, 0, 0, data.length);
- } else {
- tmp = new Buffer(inBuffer.length + data.length);
- inBuffer.copy(tmp, 0, 0, inBuffer.length);
- data.copy(tmp, inBuffer.length, 0, data.length);
- inBuffer = tmp;
- }
- while ((curIdx = inBuffer.indexOf(endMarker)) > -1) {
- // Closing handshake
- if (inBuffer[0] == endMarker && inBuffer[1] && inBuffer[1] == beginMarker) {
- server.options.logger('HttpClient :: WebSocket received closing handshake. Closing connection.', grappler.LOG.INFO);
- self.disconnect();
- return;
- }
- if (inBuffer[0] != beginMarker) {
- server.options.logger('HttpClient :: WebSocket data incorrectly framed by UA. Closing connection.', grappler.LOG.WARN);
- self.disconnect();
- return;
- }
- tmp = new Buffer(curIdx-1);
- inBuffer.copy(tmp, 0, 1, curIdx);
- server.emit('data', tmp, self);
- inBuffer = inBuffer.slice(curIdx+1, inBuffer.length);
- }
- });
- } else { // Plain HTTP connection (everything else)
- switch (req.method.toUpperCase()) {
- case "GET":
- var isMultipart = (req.headers.accept && req.headers.accept.indexOf('multipart/x-mixed-replace') > -1),
- isSSEDOM = (req.headers.accept && req.headers.accept.indexOf('application/x-dom-event-stream') > -1),
- isSSE = ((req.headers.accept && req.headers.accept.indexOf('text/event-stream') > -1) || isSSEDOM);
- req.connection.setKeepAlive(true, server.options.pingInterval);
- if (isMultipart) { // Multipart (x-mixed-replace)
- res.setSecureCookie('grappler', self._id);
- self._makePerm();
- server.options.logger('HttpClient :: Using multipart for client id ' + self._id, grappler.LOG.INFO);
- req.connection.setNoDelay(true);
- res.useChunkedEncodingByDefault = false;
- res.writeHead(200, {
- 'Content-Type': 'multipart/x-mixed-replace;boundary="grappler"',
- 'Connection': 'keep-alive',
- 'Expires': 'Fri, 01 Jan 1990 00:00:00 GMT',
- 'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
- 'Pragma': 'no-cache'
- });
- res.write("--grappler\n");
- self.write(""); // send a ping packet immediately
- req.connection.removeAllListeners('timeout'); // remove the server's protocol detection timeout listener
- req.connection.setTimeout(server.options.pingInterval);
- req.connection.addListener('timeout', function() {
- self.write("");
- req.connection.setTimeout(server.options.pingInterval);
- });
- server.emit('connection', this);
- } else if (isSSE) { // Server-Side Events
- res.setSecureCookie('grappler', self._id);
- self._makePerm();
- server.options.logger('HttpClient :: Using server-side events for client id ' + self._id, grappler.LOG.INFO);
- res.writeHead(200, {
- 'Content-Type': (isSSEDOM ? 'application/x-dom-event-stream' : 'text/event-stream'),
- 'Expires': 'Fri, 01 Jan 1990 00:00:00 GMT',
- 'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
- 'Pragma': 'no-cache'
- });
- res.write(": grappler stream\n\n"); // no retry field for now...
- req.connection.setTimeout(0);
- req.connection.removeAllListeners('timeout'); // remove the server's protocol detection timeout listener
- req.connection.setTimeout(server.options.pingInterval);
- req.connection.addListener('timeout', function() {
- res.write(":\n\n"); // send a comment line to keep the connection alive, especially for when client is behind a proxy
- req.connection.setTimeout(server.options.pingInterval);
- });
- server.emit('connection', this);
- } else { // Long poll
- var cookie = undefined;
- try {
- cookie = req.getSecureCookie('grappler');
- } catch(e) {}
-
- if (!cookie) {
- res.setSecureCookie('grappler', self._id);
- // Reset connection to make sure the session cookie is set
- self.write("");
- } else {
- var isSubsequent = false;
- if (!server.connections[cookie]) {
- // Initial connection
- server.connections[cookie] = this;
- server.emit('connection', this);
- server.connections[cookie]._checkQueue = function() {
- // Send any pending data sent to this long poll client
- if (server.connections[cookie]._queue && server.connections[cookie]._queue.length)
- server.connections[cookie].write();
- };
- } else {
- // The original HttpClient just needs to reuse subsequent connections'
- // ServerRequest and ServerResponse objects so that we can still write
- // the client using the original HttpClient instance
- server.connections[cookie]._request = self._request;
- server.connections[cookie]._response = self._response;
- isSubsequent = true;
- }
- // Set a timeout to assume the client has permanently disconnected if they
- // do not reconnect after a certain period of time
- server.connections[cookie]._request.connection.addListener('end', function() {
- if (server.connections[cookie].pollWaiting)
- clearTimeout(server.connections[cookie].pollWaiting);
- server.connections[cookie].pollWaiting = setTimeout(function() {
- if (server.connections[cookie]._request.connection.readyState != 'open') {
- try {
- server.connections[cookie].emit('close');
- req.connection.end();
- req.connection.destroy();
- delete server.connections[cookie];
- } catch(e) {}
- }
- }, server.options.pingInterval);
- });
- server.options.logger('HttpClient :: Client prefers id of ' + cookie + ' instead of ' + self._id + (isSubsequent ? ' (subsequent)' : ''), grappler.LOG.INFO);
- server.options.logger('HttpClient :: Using long polling for client id ' + cookie, grappler.LOG.INFO);
- }
- }
- break;
- case "POST":
- var cookie;
- // Authenticate the validity of the "send message" request as best as we can
- if (!(cookie = req.getSecureCookie('grappler')) || typeof server.connections[cookie] == 'undefined' ||
- req.connection.remoteAddress != server.connections[cookie].remoteAddress ||
- !(server.connections[cookie].state & grappler.STATE.PROTO_HTTP)) {
- server.options.logger('HttpClient :: Invalid POST request due to bad cookie: ' + (cookie ? cookie : '(cookie not set)'), grappler.LOG.WARN);
- self.disconnect();
- return;
- }
- var inBuffer = null;
- req.addListener('data', function(data) {
- if (!inBuffer || inBuffer.length == 0) {
- inBuffer = new Buffer(data.length);
- data.copy(inBuffer, 0, 0, data.length);
- } else {
- var tmp = new Buffer(inBuffer.length + data.length);
- inBuffer.copy(tmp, 0, 0, inBuffer.length);
- data.copy(tmp, inBuffer.length, 0, data.length);
- inBuffer = tmp;
- }
- });
- req.addListener('end', function() {
- res.writeHead(200);
- res.end();
- self.disconnect();
- server.emit('data', inBuffer, server.connections[cookie]);
- });
- break;
- case "OPTIONS": // preflighted cross-origin request (see: https://developer.mozilla.org/en/HTTP_access_control)
- var headers = {};
- headers['Access-Control-Allow-Origin'] = req.headers.origin;
- headers['Access-Control-Allow-Credentials'] = 'true';
- if (req.headers['Access-Control-Request-Headers'])
- headers['Access-Control-Allow-Headers'] = req.headers['Access-Control-Request-Headers'];
- if (req.headers['Access-Control-Request-Method'])
- headers['Access-Control-Allow-Methods'] = req.headers['Access-Control-Request-Method'];
- res.writeHead(200, headers);
- res.end();
- break;
- default: // Unknown Method
- res.writeHead(405);
- res.end();
- break;
- }
- }
+var HttpClient = function(req, res, upgradeBody) {
+ var client = this.client = req.connection.client,
+ self = this,
+ server = client.server;
+ this.server = server;
+ this._req = req;
+ this._res = res;
+ this.remoteAddress = client.remoteAddress;
+ this.state = client.state;
+
+ // Ignore favicon requests since they weren't handled by the user's callback
+ if (req.url === "/favicon.ico") {
+ res.writeHead(404);
+ res.end();
+ return;
+ }
+
+ // Check for a permitted origin
+ if (req.headers.origin && !isAllowed(server.options.origins, req.headers.origin)) {
+ server.options.logger('HttpClient :: Denied client ' + client._id + ' due to disallowed origin (\'' + req.headers.origin + '\')', common.LOG.INFO);
+ if (res instanceof http.ServerResponse) {
+ res.writeHead(403);
+ res.end();
+ }
+ req.connection.destroy();
+ return;
+ }
+
+ if (req.headers.upgrade) {
+ if (req.headers.upgrade === 'WebSocket')
+ self.state |= common.STATE.PROTO_WEBSOCKET;
+ else {
+ self.disconnect();
+ server.options.logger('HttpClient :: Unrecognized HTTP upgrade request: ' + req.headers.upgrade, common.LOG.WARN);
+ }
+ }
+
+ // Check for a WebSocket request
+ if (self.state & common.STATE.PROTO_WEBSOCKET) {
+ self._makePerm();
+ var draft = (typeof req.headers['sec-websocket-key1'] !== 'undefined' &&
+ typeof req.headers['sec-websocket-key2'] !== 'undefined' ? 76 : 75),
+ outgoingdata = ['HTTP/1.1 101 Web' + (draft === 75 ? ' ' : '') + 'Socket Protocol Handshake',
+ 'Upgrade: WebSocket',
+ 'Connection: Upgrade'],
+ beginMsg = true, curMsg;
+ server.options.logger('HttpClient :: Using WebSocket draft ' + draft + ' for client id ' + client._id, common.LOG.INFO);
+
+ if (draft === 75) {
+ outgoingdata = outgoingdata.concat(['WebSocket-Origin: ' + req.headers.origin, 'WebSocket-Location: ws://' + req.headers.host + req.url]);
+ outgoingdata = outgoingdata.concat(['', '']).join('\r\n');
+ } else if (draft === 76) {
+ var strkey1 = req.headers['sec-websocket-key1'],
+ strkey2 = req.headers['sec-websocket-key2'],
+ key1 = parseInt(strkey1.replace(/[^\d]/g, ""), 10),
+ key2 = parseInt(strkey2.replace(/[^\d]/g, ""), 10),
+ spaces1 = strkey1.replace(/[^\ ]/g, "").length,
+ spaces2 = strkey2.replace(/[^\ ]/g, "").length;
+
+ if (spaces1 === 0 || spaces2 === 0 || key1 % spaces1 !== 0 || key2 % spaces2 !== 0 && upgradeBody.length === 8) {
+ server.options.logger('HttpClient :: WebSocket request contained an invalid key. Closing connection.', common.LOG.WARN);
+ self.disconnect();
+ return;
+ }
+
+ outgoingdata = outgoingdata.concat(['Sec-WebSocket-Origin: ' + req.headers.origin, 'Sec-WebSocket-Location: ws://' + req.headers.host + req.url]);
+ if (req.headers['Sec-WebSocket-Protocol'])
+ outgoingdata = outgoingdata.concat(['Sec-WebSocket-Protocol: ' + req.headers['Sec-WebSocket-Protocol']]);
+
+ var hash = crypto.createHash('md5');
+ hash.update(pack(parseInt(key1/spaces1)));
+ hash.update(pack(parseInt(key2/spaces2)));
+ hash.update(upgradeBody.toString('binary'));
+ outgoingdata = outgoingdata.concat(['', '']).join('\r\n') + hash.digest('binary');
+ }
+ self._makePerm();
+
+ req.connection.setNoDelay(true);
+ req.connection.setKeepAlive(true, server.options.pingInterval);
+ req.connection.write(outgoingdata, (draft === 75 ? 'ascii' : 'binary'));
+ self.handshake = true;
+
+ if (req.connection.readyState === 'open')
+ server.emit('connection', this);
+
+ req.connection.on('data', function(data) {
+ var beginMarker = 0, endMarker = 255, idxEnd;
+
+ while (true) {
+ if (beginMsg) {
+ beginMsg = false;
+ var byebye = false;
+ if (data[0] === endMarker && typeof data[1] !== undefined && data[1] === beginMarker) {
+ server.options.logger('HttpClient :: WebSocket received closing handshake. Closing connection.', common.LOG.INFO);
+ byebye = true;
+ } else if (data[0] !== beginMarker) {
+ server.options.logger('HttpClient :: WebSocket data incorrectly framed by UA. Closing connection.', common.LOG.WARN);
+ byebye = true;
+ }
+ if (byebye) {
+ self.disconnect();
+ return;
+ }
+ curMsg = new Message();
+ self.emit('message', curMsg);
+ data = data.slice(1);
+ }
+
+ idxEnd = data.indexOf(endMarker);
+ idxStart = data.indexOf(beginMarker);
+
+ if (idxEnd > -1) {
+ curMsg.emit('data', data.slice(0, idxEnd));
+ curMsg.emit('end');
+ beginMsg = true;
+ if (idxEnd === data.length-1)
+ break;
+ data = data.slice(idxEnd+1);
+ } else {
+ curMsg.emit('data', data);
+ break;
+ }
+ }
+ });
+ } else { // Plain HTTP connection (everything else)
+ switch (req.method.toUpperCase()) {
+ case "GET":
+ var isMultipart = (req.headers.accept && req.headers.accept.indexOf('multipart/x-mixed-replace') > -1),
+ isSSEDOM = (req.headers.accept && req.headers.accept.indexOf('application/x-dom-event-stream') > -1),
+ isSSE = ((req.headers.accept && req.headers.accept.indexOf('text/event-stream') > -1) || isSSEDOM);
+ req.connection.setKeepAlive(true, server.options.pingInterval);
+ if (isMultipart) { // Multipart (x-mixed-replace)
+ res.setSecureCookie('grappler', client._id);
+ self._makePerm();
+ server.options.logger('HttpClient :: Using multipart for client id ' + client._id, common.LOG.INFO);
+ req.connection.setNoDelay(true);
+ res.useChunkedEncodingByDefault = false;
+ res.writeHead(200, {
+ 'Content-Type': 'multipart/x-mixed-replace;boundary="grappler"',
+ 'Connection': 'keep-alive',
+ 'Expires': 'Fri, 01 Jan 1990 00:00:00 GMT',
+ 'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
+ 'Pragma': 'no-cache'
+ });
+ res.write("--grappler\n");
+ self.write(""); // send a ping packet immediately
+ req.connection.removeAllListeners('timeout'); // remove the server's protocol detection timeout listener
+ req.connection.setTimeout(server.options.pingInterval);
+ req.connection.on('timeout', function() {
+ self.write("");
+ req.connection.setTimeout(server.options.pingInterval);
+ });
+ server.emit('connection', this);
+ } else if (isSSE) { // Server-Side Events
+ res.setSecureCookie('grappler', client._id);
+ self._makePerm();
+ server.options.logger('HttpClient :: Using server-side events for client id ' + client._id, common.LOG.INFO);
+ res.writeHead(200, {
+ 'Content-Type': (isSSEDOM ? 'application/x-dom-event-stream' : 'text/event-stream'),
+ 'Expires': 'Fri, 01 Jan 1990 00:00:00 GMT',
+ 'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
+ 'Pragma': 'no-cache'
+ });
+ res.write(": grappler stream\n\n"); // no retry field for now...
+ req.connection.setTimeout(0);
+ req.connection.removeAllListeners('timeout'); // remove the server's protocol detection timeout listener
+ req.connection.setTimeout(server.options.pingInterval);
+ req.connection.on('timeout', function() {
+ res.write(":\n\n"); // send a comment line to keep the connection alive, especially for when client is behind a proxy
+ req.connection.setTimeout(server.options.pingInterval);
+ });
+ server.emit('connection', this);
+ } else { // Long poll
+ var cookie = undefined;
+ try {
+ cookie = req.getSecureCookie('grappler');
+ } catch(e) {}
+
+ if (!cookie) {
+ res.setSecureCookie('grappler', client._id);
+ // Reset connection to make sure the session cookie is set
+ self.write("");
+ } else {
+ var isSubsequent = false,
+ conn = server.connections.get(cookie);
+ if (!conn) {
+ // Initial connection
+ server.connections.set(cookie, this);
+ conn = this;
+ server.emit('connection', this);
+ conn._checkQueue = function() {
+ // Send any pending data sent to this long poll client
+ if (conn._queue && conn._queue.length)
+ conn.write();
+ };
+ } else {
+ // The original HttpClient just needs to reuse subsequent connections'
+ // ServerRequest and ServerResponse objects so that we can still write
+ // the client using the original HttpClient instance
+ conn._req = self._req;
+ conn._res = self._res;
+ isSubsequent = true;
+ }
+ // Set a timeout to assume the client has permanently disconnected if they
+ // do not reconnect after a certain period of time
+ conn._req.connection.on('end', function() {
+ if (conn.pollWaiting)
+ clearTimeout(conn.pollWaiting);
+ conn.pollWaiting = setTimeout(function() {
+ if (conn._req.connection.readyState !== 'open') {
+ try {
+ conn.emit('end');
+ req.connection.end();
+ req.connection.destroy();
+ server.connections.delete(cookie);
+ } catch(e) {}
+ }
+ }, server.options.pingInterval);
+ });
+ server.options.logger('HttpClient :: Client prefers id of ' + cookie + ' instead of ' + client._id + (isSubsequent ? ' (subsequent)' : ''), common.LOG.INFO);
+ server.options.logger('HttpClient :: Using long polling for client id ' + cookie, common.LOG.INFO);
+ }
+ }
+ break;
+ case "POST":
+ var cookie;
+ // Authenticate the validity of the "send message" request as best as we can
+ if (!(cookie = req.getSecureCookie('grappler')) || typeof server.connections.get(cookie) === 'undefined' ||
+ req.connection.remoteAddress !== server.connections.get(cookie).remoteAddress ||
+ !(server.connections.get(cookie).state & common.STATE.PROTO_HTTP)) {
+ server.options.logger('HttpClient :: Invalid POST request due to bad cookie: ' + (cookie ? cookie : '(cookie not set)'), common.LOG.WARN);
+ self.disconnect();
+ return;
+ }
+ var msg = new Message();
+ self.emit('message', msg);
+ req.on('data', function(data) {
+ msg.emit('data', data);
+ });
+ req.on('end', function() {
+ res.writeHead(200);
+ res.end();
+ self.disconnect();
+ msg.emit('end');
+ });
+ break;
+ case "OPTIONS": // preflighted cross-origin request (see: https://developer.mozilla.org/en/HTTP_access_control)
+ var headers = {};
+ headers['Access-Control-Allow-Origin'] = req.headers.origin;
+ headers['Access-Control-Allow-Credentials'] = 'true';
+ if (req.headers['Access-Control-Request-Headers'])
+ headers['Access-Control-Allow-Headers'] = req.headers['Access-Control-Request-Headers'];
+ if (req.headers['Access-Control-Request-Method'])
+ headers['Access-Control-Allow-Methods'] = req.headers['Access-Control-Request-Method'];
+ res.writeHead(200, headers);
+ res.end();
+ break;
+ default: // Unknown Method
+ res.writeHead(405);
+ res.end();
+ break;
+ }
+ }
};
-sys.inherits(HttpClient, EventEmitter);
-exports.HttpClient = HttpClient;
+util.inherits(HttpClient, EventEmitter);
+module.exports = HttpClient;
HttpClient.prototype._makePerm = function() {
- var self = this;
- this.state = (this.state & ~grappler.STATE.TEMP);
- this.server.connections[this._id] = this; // lazy add to connections list
- this._request.connection.addListener('drain', function() { self.emit('drain'); });
+ var self = this;
+ this.state = (this.state & ~common.STATE.TEMP);
+ this.server.connections.set(this.client._id, this); // lazy add to connections list
+ this._req.connection.on('drain', function() { self.emit('drain'); });
};
HttpClient.prototype.write = function(data, encoding) {
- var isMultipart = (this._request.headers.accept && this._request.headers.accept.indexOf('multipart/x-mixed-replace') > -1),
- isSSEDOM = (this._request.headers.accept && this._request.headers.accept.indexOf('application/x-dom-event-stream') > -1),
- isSSE = ((this._request.headers.accept && this._request.headers.accept.indexOf('text/event-stream') > -1) || isSSEDOM),
- self = this,
- retVal = true;
-
- try {
- if (this.state & grappler.STATE.PROTO_WEBSOCKET) { // WebSocket
- if (data.length > 0) {
- if (!self.handshake) {
- process.nextTick(function() { self.write(data, encoding); });
- return;
- }
- this._request.connection.write('\x00', 'binary');
- this._request.connection.write(data, 'utf8');
- retVal = this._request.connection.write('\xff', 'binary');
- }
- } else if (isMultipart) { // multipart (x-mixed-replace)
- this._response.write("Content-Type: " + (data instanceof Buffer && (!encoding || encoding == 'binary') ? "application/octet-stream" : "text/plain") + "\nContent-Length: " + data.length + "\n\n");
- this._response.write(data, encoding);
- retVal = this._response.write("\n--grappler\n");
- } else if (isSSE) { // Server-Sent Events -- via JS or DOM
- if (isSSEDOM)
- this._response.write("Event: grappler-data\n");
- this._response.write("data: ");
- this._response.write(data, encoding);
- retVal = this._response.write("\n\n");
- } else if (typeof self._checkQueue != 'undefined') { // long poll
- if (!this._queue)
- this._queue = [];
-
- // Always append to the initial connection's write queue.
- // Queueing every piece of data provides consistency and correct ordering of incoming writes, no matter
- // if the long poll client is in the process of reconnecting or not.
- //
- // TODO: Have a callback for write to know if the message was successfully sent?
- // For example, a bunch of writes could be queued up for a long poll client, but they never end up
- // reconnecting -- thus losing all of those messages. However, it is currently assumed they were
- // in fact sent successfully. We should let the sender know they were not received by the recipient.
- if (arguments.length > 0)
- this._queue.push([data, encoding]);
-
- if (this._request.connection.readyState == 'open') {
- data = this._queue.shift();
- encoding = data[1];
- data = data[0];
- this._response.writeHead(200, {
- 'Content-Type': (data instanceof Buffer && (!encoding || encoding == 'binary') ? "application/octet-stream" : "text/plain"),
- 'Content-Length': data.length,
- 'Connection': 'keep-alive',
- 'Expires': 'Fri, 01 Jan 1990 00:00:00 GMT',
- 'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',