From c8e8c6983b657ce006b871da8cabf64bbbe010cd Mon Sep 17 00:00:00 2001 From: Diego Araos Date: Mon, 7 May 2012 05:16:18 -0400 Subject: [PATCH] working on presence/private channels --- config.template.js | 4 +- lib/configure.js | 17 ++++ lib/mustekala.client.js | 190 ++++++++++++++++++++++++++++++++++++++++ lib/mustekala.js | 47 ++++++++++ lib/routes.js | 73 +++++++++++++++ lib/socket.js | 56 ++++++++++++ mustekala.js | 174 ++---------------------------------- package.json | 9 +- public/example.html | 6 +- public/jquery.js | 4 + public/mustekala.js | 142 ------------------------------ 11 files changed, 405 insertions(+), 317 deletions(-) create mode 100644 lib/configure.js create mode 100644 lib/mustekala.client.js create mode 100644 lib/mustekala.js create mode 100644 lib/routes.js create mode 100644 lib/socket.js create mode 100644 public/jquery.js delete mode 100644 public/mustekala.js diff --git a/config.template.js b/config.template.js index 7ec6585..4848d18 100644 --- a/config.template.js +++ b/config.template.js @@ -1,4 +1,4 @@ module.exports={ - 'password': 'bGINGS/(ADNfg78GASIMDbkASbj' // replace with your own - ,'port': 4567 // default: 4567 + password: 'bGINGS/(ADNfg78GASIMDbkASbj' // replace with your own + ,port: 4567 // default: 4567 } diff --git a/lib/configure.js b/lib/configure.js new file mode 100644 index 0000000..48e5c5a --- /dev/null +++ b/lib/configure.js @@ -0,0 +1,17 @@ +app.configure(function(){ + // app.set('views', __dirname + '/views'); + // app.set('view engine', 'jade'); + app.use(express.bodyParser()); + app.use(express.methodOverride()); + app.use(app.router); + app.use(express.static(process.cwd() + '/public')); +}); + +app.configure('development', function(){ + app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); +}); + +app.configure('production', function(){ + // app.get('/example.html', function(req, res) { res.redirect('/'); }); // Disable /example.html on production + app.use(express.errorHandler()); +}); \ No newline at end of file diff --git a/lib/mustekala.client.js b/lib/mustekala.client.js new file mode 100644 index 0000000..983df9c --- /dev/null +++ b/lib/mustekala.client.js @@ -0,0 +1,190 @@ +var Mustekala=function(setup) { + setup=setup || {}; + var m={ + 'version': '0.1.0' + ,'connected': false + ,'socket': {} + ,'events': {} + // configurable settings + ,'authUrl': setup['authUrl'] || false // auth url for private/presence channels + ,'debug': setup['debug'] || 0 // debugging (verbose levels: 0,1,2) + } + + m.unbindEvents=function() { + m.events={ + 'connect': [] + ,'disconnect': [] + ,'trigger': [] + ,'subscribe': [] + ,'unsubscribe': [] + ,'log': [] + } + } + m.unbindEvents(); + m.connect=function(connectListener) { + // connect to socket + m.unbindEvents(); + m.onConnect(connectListener); + m.socket=io.connect('/mustekala'); // TODO: this must be provided by the app configuration on setup + m.socket.on('connect', function() { + m.connected = true; + m.triggerEvent('connect'); + }); + // set event listeners + m.socket.on('log', function(data) { + m.triggerEvent('log', data); + }); + m.socket.on('subscribe', function(channel) { + m.triggerEvent('subscribe', channel); + }); + m.socket.on('unsubscribe', function(channel) { + m.triggerEvent('unsubscribe', channel); + }); + m.socket.on('trigger', function(channel, action, data) { + m.triggerEvent('trigger', channel, action, data); + + var key='-'+channel+'-'+action; + if(m.events[key]) + m.triggerEvent(key, data); + }); + m.socket.on('disconnect', function() { + m.connected = false; + m.triggerEvent('disconnect'); + }) + } + m.subscribe=function(channel) { + if(m.connected) { + var regexp=new RegExp(/^(presence|private)@(.+)$/); + var regexpChannel=regexp.exec(channel); + if(regexpChannel) { + // Private or Presence channels: + var channelType=regexpChannel[1]; + var channel=regexpChannel[2]; + if(m.authUrl) { + AjaxRequest.post({ + 'url': m.authUrl + ,'onSuccess': function(response) { + try { + authKey=JSON.parse(response.responseText).authKey; + if(authKey) { + m.socket.emit('subscribe',channel,channelType,authKey); + } else { + throw true + } + } catch(e) { + throw 'Unable to parse your '+channelType+' channel authorization key.'; + } + }, 'onError': function() { + throw 'Unable to authentificate your '+channelType+' channel subscription.'; + } + }) + return new MustekalaChannel(channel); + } else { + throw 'You must provide an authUrl when instancing Mustekala to create private or presence channels.' + } + } else { + // Public channel: + m.socket.emit('subscribe', channel); + return new MustekalaChannel(channel); + } + } else { + throw 'Mustekala is not connected.'; + if(m.debug) + console.log('m.subscribe', 'Not connected.'); + } + } + // TODO: m.disconnet() + /* + * This is for testing purposes, do not use it in your front-end code on production + */ + m.trigger=function(password, channel, action, data) { + if(m.connected) { + if(m.debug) + console.log('m.trigger', 'WARNING: This is for testing purposes only, do not use on production!'); + m.socket.emit('trigger', password, channel, action, data); + } else { + throw 'Mustekala is not connected.'; + if(m.debug) + console.log('m.trigger', 'Not connected.'); + } + } + + // Events + m.onConnect=function(listener) { + m.events['connect'].push(listener); + if(m.debug>1) + console.log('m.onConnect'); + } + m.onDisconnect=function(listener) { + m.events['disconnect'].push(listener); + if(m.debug>1) + console.log('m.onDisconnect'); + } + m.onLog=function(listener) { + m.events['log'].push(listener); + if(m.debug>1) + console.log('m.onDisconnect'); + } + m.onSubscribe=function(listener) { + m.events['subscribe'].push(listener); + if(m.debug>1) + console.log('m.onSubscribe'); + } + m.onUnsubscribe=function(listener) { + m.events['unsubscribe'].push(listener); + if(m.debug>1) + console.log('m.onUnsubscribe'); + } + m.onTrigger=function(listener) { + m.events['trigger'].push(listener); + if(m.debug>1) + console.log('m.onTrigger'); + } + m.triggerEvent=function() { + if(m.debug) + console.log('m.triggerEvent',m.triggerEvent.arguments); + + event=m.triggerEvent.arguments[0]; + args=[]; + for(var i=1; i.on', action); + } + var key='-'+channel+'-'+action; + if(!m.events[key]) + m.events[key]=[]; + m.events[key].push(function(data) { + handler(data); + }); + } + // TODO: add more methods & attributes (status, unsubscribe, etc.) + } + } + + return m; +}; + +/*! AjaxRequest Author: http://www.ajaxtoolbox.com/ */ +function AjaxRequest(){var a=new Object();a.timeout=null;a.generateUniqueUrl=true;a.url=window.location.href;a.method="GET";a.async=true;a.username=null;a.password=null;a.parameters=new Object();a.requestIndex=AjaxRequest.numAjaxRequests++;a.responseReceived=false;a.groupName=null;a.queryString="";a.responseText=null;a.responseXML=null;a.status=null;a.statusText=null;a.aborted=false;a.xmlHttpRequest=null;a.onTimeout=null;a.onLoading=null;a.onLoaded=null;a.onInteractive=null;a.onComplete=null;a.onSuccess=null;a.onError=null;a.onGroupBegin=null;a.onGroupEnd=null;a.xmlHttpRequest=AjaxRequest.getXmlHttpRequest();if(a.xmlHttpRequest==null){return null}a.xmlHttpRequest.onreadystatechange=function(){if(a==null||a.xmlHttpRequest==null){return}if(a.xmlHttpRequest.readyState==1){a.onLoadingInternal(a)}if(a.xmlHttpRequest.readyState==2){a.onLoadedInternal(a)}if(a.xmlHttpRequest.readyState==3){a.onInteractiveInternal(a)}if(a.xmlHttpRequest.readyState==4){a.onCompleteInternal(a)}};a.onLoadingInternalHandled=false;a.onLoadedInternalHandled=false;a.onInteractiveInternalHandled=false;a.onCompleteInternalHandled=false;a.onLoadingInternal=function(){if(a.onLoadingInternalHandled){return}AjaxRequest.numActiveAjaxRequests++;if(AjaxRequest.numActiveAjaxRequests==1&&typeof(window.AjaxRequestBegin)=="function"){AjaxRequestBegin()}if(a.groupName!=null){if(typeof(AjaxRequest.numActiveAjaxGroupRequests[a.groupName])=="undefined"){AjaxRequest.numActiveAjaxGroupRequests[a.groupName]=0}AjaxRequest.numActiveAjaxGroupRequests[a.groupName]++;if(AjaxRequest.numActiveAjaxGroupRequests[a.groupName]==1&&typeof(a.onGroupBegin)=="function"){a.onGroupBegin(a.groupName)}}if(typeof(a.onLoading)=="function"){a.onLoading(a)}a.onLoadingInternalHandled=true};a.onLoadedInternal=function(){if(a.onLoadedInternalHandled){return}if(typeof(a.onLoaded)=="function"){a.onLoaded(a)}a.onLoadedInternalHandled=true};a.onInteractiveInternal=function(){if(a.onInteractiveInternalHandled){return}if(typeof(a.onInteractive)=="function"){a.onInteractive(a)}a.onInteractiveInternalHandled=true};a.onCompleteInternal=function(){if(a.onCompleteInternalHandled||a.aborted){return}a.onCompleteInternalHandled=true;AjaxRequest.numActiveAjaxRequests--;if(AjaxRequest.numActiveAjaxRequests==0&&typeof(window.AjaxRequestEnd)=="function"){AjaxRequestEnd(a.groupName)}if(a.groupName!=null){AjaxRequest.numActiveAjaxGroupRequests[a.groupName]--;if(AjaxRequest.numActiveAjaxGroupRequests[a.groupName]==0&&typeof(a.onGroupEnd)=="function"){a.onGroupEnd(a.groupName)}}a.responseReceived=true;a.status=a.xmlHttpRequest.status;a.statusText=a.xmlHttpRequest.statusText;a.responseText=a.xmlHttpRequest.responseText;a.responseXML=a.xmlHttpRequest.responseXML;if(typeof(a.onComplete)=="function"){a.onComplete(a)}if(a.xmlHttpRequest.status==200&&typeof(a.onSuccess)=="function"){a.onSuccess(a)}else{if(typeof(a.onError)=="function"){a.onError(a)}}delete a.xmlHttpRequest.onreadystatechange;a.xmlHttpRequest=null};a.onTimeoutInternal=function(){if(a!=null&&a.xmlHttpRequest!=null&&!a.onCompleteInternalHandled){a.aborted=true;a.xmlHttpRequest.abort();AjaxRequest.numActiveAjaxRequests--;if(AjaxRequest.numActiveAjaxRequests==0&&typeof(window.AjaxRequestEnd)=="function"){AjaxRequestEnd(a.groupName)}if(a.groupName!=null){AjaxRequest.numActiveAjaxGroupRequests[a.groupName]--;if(AjaxRequest.numActiveAjaxGroupRequests[a.groupName]==0&&typeof(a.onGroupEnd)=="function"){a.onGroupEnd(a.groupName)}}if(typeof(a.onTimeout)=="function"){a.onTimeout(a)}delete a.xmlHttpRequest.onreadystatechange;a.xmlHttpRequest=null}};a.process=function(){if(a.xmlHttpRequest!=null){if(a.generateUniqueUrl&&a.method=="GET"){a.parameters.AjaxRequestUniqueId=new Date().getTime()+""+a.requestIndex}var c=null;for(var b in a.parameters){if(a.queryString.length>0){a.queryString+="&"}a.queryString+=encodeURIComponent(b)+"="+encodeURIComponent(a.parameters[b])}if(a.method=="GET"){if(a.queryString.length>0){a.url+=((a.url.indexOf("?")>-1)?"&":"?")+a.queryString}}a.xmlHttpRequest.open(a.method,a.url,a.async,a.username,a.password);if(a.method=="POST"){if(typeof(a.xmlHttpRequest.setRequestHeader)!="undefined"){a.xmlHttpRequest.setRequestHeader("Content-type","application/x-www-form-urlencoded")}c=a.queryString}if(a.timeout>0){setTimeout(a.onTimeoutInternal,a.timeout)}a.xmlHttpRequest.send(c)}};a.handleArguments=function(b){for(var c in b){if(typeof(a[c])=="undefined"){a.parameters[c]=b[c]}else{a[c]=b[c]}}};a.getAllResponseHeaders=function(){if(a.xmlHttpRequest!=null){if(a.responseReceived){return a.xmlHttpRequest.getAllResponseHeaders()}alert("Cannot getAllResponseHeaders because a response has not yet been received")}};a.getResponseHeader=function(b){if(a.xmlHttpRequest!=null){if(a.responseReceived){return a.xmlHttpRequest.getResponseHeader(b)}alert("Cannot getResponseHeader because a response has not yet been received")}};return a}AjaxRequest.getXmlHttpRequest=function(){if(window.XMLHttpRequest){return new XMLHttpRequest()}else{if(window.ActiveXObject){ +/*@cc_on @*/ +/*@if(@_jscript_version >=5) +try{return new ActiveXObject("Msxml2.XMLHTTP");}catch(e){try{return new ActiveXObject("Microsoft.XMLHTTP");}catch(E){return null;}}@end @*/ +}else{return null}}};AjaxRequest.isActive=function(){return(AjaxRequest.numActiveAjaxRequests>0)};AjaxRequest.get=function(a){AjaxRequest.doRequest("GET",a)};AjaxRequest.post=function(a){AjaxRequest.doRequest("POST",a)};AjaxRequest.doRequest=function(c,a){if(typeof(a)!="undefined"&&a!=null){var b=new AjaxRequest();b.method=c;b.handleArguments(a);b.process()}};AjaxRequest.submit=function(a,b){var d=new AjaxRequest();if(d==null){return false}var c=AjaxRequest.serializeForm(a);d.method=a.method.toUpperCase();d.url=a.action;d.handleArguments(b);d.queryString=c;d.process();return true};AjaxRequest.serializeForm=function(b){var e=b.elements;var a=e.length;var g="";this.addField=function(h,i){if(g.length>0){g+="&"}g+=encodeURIComponent(h)+"="+encodeURIComponent(i)};for(var d=0;d=0){this.addField(f.name,f.options[f.selectedIndex].value)}break;case"select-multiple":for(var c=0;c'+mustekala.github+''); +}); + +// TODO: Disable on production +app.post('/example/auth', function(req, res) { + var exampleUser = { + id: '12345' + ,data: { + name: 'Joseph' + ,nickname: 'joe' + } + } + require('request').post({ + url: '/mustekala/authentificate' + ,body: { + password: mustekala.config.password + ,user: exampleUser + } + },function(error, response, body) { + console.log(body); + }); + res.end(JSON.stringify({'authKey': '12345'})); +}); diff --git a/lib/socket.js b/lib/socket.js new file mode 100644 index 0000000..bee2281 --- /dev/null +++ b/lib/socket.js @@ -0,0 +1,56 @@ +var io = mustekala.io = require('socket.io').listen(app); +io.configure('development', function(){ + io.set('transports', ['websocket']); +}); +io.configure('production', function(){ + io.enable('browser client minification'); + io.enable('browser client etag'); + io.disable('browser client gzip'); // Mustekala won't support gzip yet + io.set('log level', 1); + io.set('transports', [ + 'websocket' + ,'flashsocket' + ,'htmlfile' + ,'xhr-polling' + ,'jsonp-polling' + ]); +}); + +// Sockets +var socket = mustekala.socket = io.of('/mustekala'); +socket.on('connection', function(client) { + console.log('*** CLIENT CONNECTED ***'); + + // client.emit('log','welcome') + var currentChannel; + + client.on('subscribe', function(channel,channelType,authKey) { + channelType=channelType||'public'; + if(channel) { + if(currentChannel) { + client.leave(currentChannel); + client.emit('unsubscribe',currentChannel); + // client.emit('log','left channel: '+currentChannel); + } + client.join(channel); + currentChannel=channel; + client.emit('subscribe', channel); + // client.emit('log','joined channel: '+channel) + + } + }); + client.on('authentificate', function() { + + }); + client.on('trigger', function(password, channel, action, data) { + console.log('***trigger.data:',data) + var result=mustekala.trigger(password, channel, action, data) + // if(result) + // client.emit('log', 'trigger sent!'); + // else + // client.emit('log', 'trigger not-sent: wrong password!'); + }); + client.on('disconnect',function(){ + // socket.emit('log','someone left!'); + }); +}); \ No newline at end of file diff --git a/mustekala.js b/mustekala.js index 8cc5b8b..51046c5 100644 --- a/mustekala.js +++ b/mustekala.js @@ -1,170 +1,8 @@ -var mustekala = { - 'version': '0.1.0' - ,'github': 'http://github.com/daraosn/mustekala' -} +var Mustekala=require('./lib/mustekala.js'); +global.express = require('express'); +global.app = module.exports = express.createServer(); +global.mustekala = new Mustekala(); +mustekala.initialize(); -try { - var config = require('./config.js'); -} catch(e) { - console.log("No 'config.js' found. Use 'config.template.js' to create one."); - process.exit(1); -} - -var express = require('express'); -var app = module.exports = express.createServer(); - -app.configure(function(){ - // app.set('views', __dirname + '/views'); - // app.set('view engine', 'jade'); - app.use(express.bodyParser()); - app.use(express.methodOverride()); - app.use(app.router); - app.use(express.static(__dirname + '/public')); -}); - -app.configure('development', function(){ - app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); -}); - -app.configure('production', function(){ - // app.get('/example.html', function(req, res) { res.redirect('/'); }); // Disable /example.html on production - app.use(express.errorHandler()); -}); - -var io = require('socket.io').listen(app); -io.configure('development', function(){ - io.set('transports', ['websocket']); -}); -io.configure('production', function(){ - io.enable('browser client minification'); - io.enable('browser client etag'); - io.disable('browser client gzip'); // Mustekala won't support gzip yet - io.set('log level', 1); - io.set('transports', [ - 'websocket' - , 'flashsocket' - , 'htmlfile' - , 'xhr-polling' - , 'jsonp-polling' - ]); -}); - - -var channels=[]; - -// Sockets -var socket=io.of('/mustekala').on('connection', function(client) { - console.log('*** CLIENT CONNECTED ***'); - - client.emit('log','welcome') - var currentChannel; - - client.on('subscribe', function(channel,presenceToken,userData) { - if(channel) { - if(currentChannel) { - client.leave(currentChannel); - client.emit('unsubscribe',currentChannel); - client.emit('log','left channel: '+currentChannel); - } - client.join(channel); - currentChannel=channel; - client.emit('subscribe', channel); - client.emit('log','joined channel: '+channel) - - } - }); - client.on('authentificate', function() { - - }); - client.on('trigger', function(password, channel, action, data) { - console.log('***trigger.data:',data) - var result=socketTrigger(password, channel, action, data) - if(result) - client.emit('log', 'trigger sent!'); - else - client.emit('log', 'trigger not-sent: wrong password!'); - }); - client.on('disconnect',function(){ - socket.emit('log','someone left!'); - }); -}); - -function socketTrigger(password, channel, action, data) { - if(password==config.password) { - if(channel) { - socket.to(channel).emit('trigger', channel, action, data); - } else { - socket.emit('trigger', channel, action, data); - } - return true; - } else { - return false; - } -} - -function socketPresencePreauthorize(password) { - if(password==config.password) { - do { var token = global.token(); } while(presenceKeys[token]); - presenceKeys[token] = new Date().getTime() + presenceKeyExpiry; - // console.log(global.dump(presenceKeys)); - return token; - } else { - return false; - } -} - -app.post('/mustekala/trigger', function(req, res) { - console.log('**** POST received') - var password=req.body.password; - var channel=req.body.channel; - var action=req.body.action; - var data=req.body.data; - var result=socketTrigger(password, channel, action, data); - console.log({'password': password, 'channel': channel, 'action': action, 'data': data}) - res.send(JSON.stringify({'success':result, 'channel': channel, 'action': action, 'data': data})); -}); - -app.post('/mustekala/presence/preauthorize', function(req, res) { - var password=req.body.password; - var result=socketPreauthorize(); - res.send(JSON.stringify({'success':result})); -}); - -app.get('/mustekala.js', function(req, res) { - // we must "cheat" socket.io to get the socket.io.js with specific transports - var ioHead={}; - var ioEnd={}; - var fakeRes = { - writeHead: function(status, headers) { - ioHead.status=status; - ioHead.headers=headers; - }, - end: function(content, encoding) { - ioEnd.content=content; - ioEnd.encoding=encoding; - finishRequest(); - } - } - // fake uri - req.url="/socket.io/socket.io.js"; - var data=io.checkRequest(req); - var mustekalaJS=require('fs').readFileSync(process.cwd()+'/public/mustekala.js'); - io.static.write(data.path, req, fakeRes); - // send js - function finishRequest() { - // inject mustekala - ioEnd.content=mustekalaJS+"\n"+ioEnd.content; - ioHead.headers['Content-Length']=ioEnd.content.length; - res.writeHead(ioHead.status, ioHead.headers) - res.end(ioEnd.content, ioEnd.encoding); - } - // TODO: verify if socket.io.js is dynamic or not to cache js. -}); - -app.get('/', function(req, res) { - res.writeHead(200, {"Content-Type": "text/html"}); - res.end('Mustekala v'+mustekala.version+'. Fork it at '+mustekala.github+''); -}); - -app.listen(config.port || 3000); +app.listen(mustekala.config.port || 3000); console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env); diff --git a/package.json b/package.json index f615540..c2bf140 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,17 @@ "author": "Diego Araos (http://wehack.it/)", "name": "mustekala", "description": "push/pull service for real-time apps", - "version": "0.0.1", + "version": "0.1.0", "homepage": "https://github.com/daraosn/mustekala", "repository": { "url": "git://github.com/daraosn/mustekala.git" }, - "dependencies": {}, + "dependencies": { + "express": "2.5.9" + , "jade": ">= 0.0.1" + , "socket.io": "0.9.6" + , "request": "2.9.x" + }, "devDependencies": {}, "optionalDependencies": {}, "engines": { diff --git a/public/example.html b/public/example.html index 21433f1..f6db6c4 100644 --- a/public/example.html +++ b/public/example.html @@ -1,13 +1,13 @@ - - + +