Implement socket.io and break apart message/session/auth libraries.

* Library modularization inspired heavily by pjriot
  (http://bitbucket.org/pjriot/nodeim)
* Remove dev server
* Remove daemon library (not working)
* Nix middleware scripts
* Nix express.js usage
Esse commit está contido em:
Joshua Gross
2011-03-03 00:09:47 -06:00
commit bd0f2562df
21 arquivos alterados com 294 adições e 1028 exclusões
+7 -26
Ver Arquivo
@@ -27,31 +27,7 @@ AjaxIM = function(options, actions) {
// upon calling the initialization function, and not set directly.//
this.settings = $.extend(defaults, options);
// === {{{AjaxIM.}}}**{{{actions}}}** ===
//
// Each individual action that the IM engine can execute is predefined here.
// By default, it merely appends the action onto the end of the {{{pollServer}}} url,
// however, it is possible to define actions individually. //The alternative actions
// will be defined upon calling the initialization function, and not set directly.//
//
// Should you define an action at a different URL, Ajax IM will determine whether
// or not this URL is within the current domain. If it is within a subdomain of
// the current domain, it will set the document.domain variable for you,
// to match a broader hostname scope; the action will continue to use {{{$.post}}}
// (the default AJAX method for Ajax IM).
//
// On the other hand, should you choose a URL outside the current domain
// Ajax IM will switch to {{{$.getJSON}}} (a get request) to avoid
// cross-domain scripting issues. This means that a server on a different
// port or at a different address will need to be able to handle GET
// requests rather than POST requests (such as how the Node.JS Ajax IM
// server works).
this.actions = $.extend({
listen: this.settings.pollServer + '/listen',
send: this.settings.pollServer + '/message',
status: this.settings.pollServer + '/status',
signoff: this.settings.pollServer + '/signoff'
}, actions);
this.socket = new io.Socket();
// We load the theme dynamically based on the passed
// settings. If the theme is set to false, we assume
@@ -282,8 +258,13 @@ $.extend(AjaxIM.prototype, {
if(this.username)
this.storage();
this.listen();
this.socket.connect();
this.socket.on('connect', this.connected);
this.socket.on('message', this.message);
this.socket.on('disconnect', this.disconnected);
},
// === {{{AjaxIM.}}}**{{{storage()}}}** ===
//
+16 -212
Ver Arquivo
@@ -1,226 +1,30 @@
#!/usr/bin/env node
var sys = require('sys'),
express = require('express'),
var http = require('http'),
io = require('./libs/socket.io'),
packages = require('./libs/packages'),
o_ = require('./libs/utils');
o_ = require('./libs/utils'),
server, socket;
o_.merge(global, require('./settings'));
try { o_.merge(global, require('./settings.local')); } catch(e) {}
try {
var daemon = require('./libs/daemon/daemon'),
start = function() {
daemon.init({
lock: PID_FILE,
stdin: '/dev/null',
stdout: LOG_FILE,
stderr: LOG_FILE,
umask: 0,
chroot: null,
chdir: '.'
});
},
stop = function() {
process.kill(parseInt(require('fs').readFileSync(PID_FILE)));
};
switch(process.argv[2]) {
case 'stop':
stop();
process.exit(0);
break;
case 'start':
if(process.argv[3])
process.env.EXPRESS_ENV = process.argv[3];
start();
break;
case 'restart':
stop();
start();
process.exit(0);
break;
case 'help':
sys.puts('Usage: node app.js [start|stop|restart]');
process.exit(0);
break;
}
} catch(e) {
sys.puts('Daemon library not found! Please compile ' +
'./libs/daemon/daemon.node if you would like to use it.');
}
var app = express.createServer(
express.methodOverride(),
express.cookieDecoder(),
express.bodyDecoder(),
require('./middleware/im')({
maxAge: 15 * 60 * 1000,
reapInterval: 60 * 1000,
authentication: require('./libs/authentication/' + AUTH_LIBRARY)
})
);
app.set('root', __dirname);
app.configure('development', function() {
app.set('view engine', 'jade');
app.set('views', __dirname + '/dev/views');
app.stack.unshift({
route: '/dev',
handle: function(req, res, next) {
req.dev = true;
next();
}
});
app.use(express.logger());
app.use('/dev', express.router(require('./dev/app')));
app.use(express.staticProvider(
require('path').join(__dirname, '../client')));
app.use(express.errorHandler({dumpExceptions: true, showStack: true}));
server = http.createServer(function(req, res) {
// Any other server code goes here.
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('');
});
server.listen(APP_PORT, APP_HOST);
app.listen(APP_PORT, APP_HOST);
var socket = io.listen(app, {
transportOptions: {
'websocket': {closeTimeout: 20000},
'flashsocket': {closeTimeout: 20000},
'htmlfile': {closeTimeout: 20000},
'xhr-multipart': {closeTimeout: 20000},
'xhr-polling': {closeTimeout: 20000},
'jsonp-polling': {closeTimeout: 20000}
}
}),
auth = require('./libs/authentication/' + AUTH_LIBRARY);
var auth_handler = require('./auth/' + AUTH_LIBRARY)(),
session_store = require('./session/' + SESSION_STORE)(),
msg_handler = require('./message/' + MESSAGE_HANDLER)(auth_handler, session_store);
socket = io.listen(server);
socket.on('connection', function(client) {
client.metadata = function(key, def) {
return client._metadata[key] || def || false;
};
client.sendTo = function(username, message) {
try {
Object.values(socket.clients).filter(function(cl) {
return username == cl.metadata('username');
}).each(function(user) {
user.send(new packages.Message(
client.metadata('username'),
message
));
});
return true;
} catch(e) {
return false;
}
};
client.on('connect', function() {
client.authenticated = false;
client.key_id = null;
client.send({type: 'auth', id: client.sessionId, key: auth.cookie});
client.on('message', function(message) {
msg_handler.message(client, message);
});
client.on('message', function(data) {
if(!data['type']) {
client.send(new packages.Error('bad packet'));
return;
}
console.log(data);
if(!client.authenticated) {
if(data.type == 'auth') {
auth.authenticate(data.id, function(info) {
if(info) {
client.authenticated = true;
client.key_id = data.id;
client._metadata = info;
auth.friends(data.id, info, function(friends) {
var friends_copy = friends.slice();
Object.values(socket.clients).filter(function(friend) {
return ~friends.indexOf(friend.metadata('username'));
}).each(function(friend) {
var username = friend.metadata('username');
friends_copy[friends_copy.indexOf(username)] =
[username, friend.metadata('status')];
}, this);
client.friends = friends_copy;
client.send({
type: 'hello',
username: client.metadata('username'),
friends: friends
});
});
} else {
client.send(new packages.Error('invalid auth'));
}
});
} else {
client.send(new packages.Error('not authenticated'));
}
} else {
// do shit.
client.send(data);
client.sendTo(client.metadata('username'), 'hello');
}
});
client.on('disconnect', function() {
msg_handler.disconnect(client, SESSION_TIMEOUT);
});
});
// Listener endpoint; handled in middleware
app.get('/listen', function(){});
app.post('/message', function(req, res) {
res.find(req.body['to'], function(user) {
if(!user)
return res.send(new packages.Error('not online'));
res.message(user, new packages.Message(
req.session.data('username'),
req.body.body
));
});
});
app.post('/message/typing', function(req, res) {
if(~packages.TYPING_STATES.indexOf('typing' + req.body['state'])) {
res.find(req.body['to'], function(user) {
if(user) {
res.message(user, new packages.Status(
req.session.data('username'),
'typing' + req.body.state
));
}
// Typing updates do not receive confirmations,
// as they are not important enough.
res.send('');
});
} else {
res.send(new packages.Error('invalid state'));
}
});
app.post('/status', function(req, res) {
if(~packages.STATUSES.indexOf(req.body['status'])) {
res.status(req.body.status, req.body.message);
res.send(new packages.Success('status updated'));
} else {
res.send(new packages.Error('invalid status'));
}
});
app.post('/signoff', function(req, res) {
res.signOff();
res.send(new packages.Success('goodbye'));
});
+17
Ver Arquivo
@@ -0,0 +1,17 @@
var ExampleAuth = function() {};
ExampleAuth.prototype.authenticate = function(client, msg, callback) {
callback({
authenticated: true,
username: 'username' + Math.round(Math.random() * 3)
});
};
ExampleAuth.prototype.friends = function(client, res, callback) {
callback(['username1', 'username2', 'username3']);
};
var instance = new ExampleAuth();
module.exports = function getInstance() {
return instance;
};
-22
Ver Arquivo
@@ -1,22 +0,0 @@
var utils = require('connect/utils');
module.exports = function(app) {
app.get('/', function(req, res) {
res.render('chat', {
locals: {}
});
});
app.get('/socket', function(req, res) {
res.render('socket', {
locals: {}
});
});
app.get('/cookie', function(req, res) {
res.send('cookie set', {
'Set-Cookie': utils.serializeCookie('sessionid', utils.uid(),
{path: '/'})
});
});
};
-1
Ver Arquivo
@@ -1 +0,0 @@
../../../client/js/
-1
Ver Arquivo
@@ -1 +0,0 @@
../../../client/themes/
-13
Ver Arquivo
@@ -1,13 +0,0 @@
!!! 5
html
head
title Ajax IM
script(src: '/js/jquery-1.4.1.js', type: 'text/javascript')
script(src: '/js/md5.js', type: 'text/javascript')
script(src: '/js/store.js', type: 'text/javascript')
script(src: '/js/cookies.js', type: 'text/javascript')
script(src: '/js/dateformat.js', type: 'text/javascript')
script(src: '/js/im.js', type: 'text/javascript')
script(type: 'text/javascript')
| $(function(){var im = AjaxIM.init({theme: "/themes/default"});});
body Hello.
-1
Ver Arquivo
@@ -1 +0,0 @@
!{body}
-10
Ver Arquivo
@@ -1,10 +0,0 @@
!!! 5
html
head
title Ajax IM
script(src: '/socket.io/socket.io.js', type: 'text/javascript')
script(src: '/js/store.js', type: 'text/javascript')
script(src: '/js/cookies.js', type: 'text/javascript')
script(src: '/js/socket_test.js', type: 'text/javascript')
body Hello.
-24
Ver Arquivo
@@ -1,24 +0,0 @@
// Cookie that stores the session ID
// Will be set as request.sessionID in `authenticate` and `friends` functions
exports.cookie = 'sessionid';
exports.authenticate = function(request, callback) {
// Verify user based on request.
// On failure, redirect user to auth form
callback({
username: 'username' + Math.floor(Math.random() * 1000),
displayname: 'John Smith',
otherinfo: 'any other relevant key/values'
});
};
exports.friends = function(request, data, callback) {
// Create a friends list based on given user data
callback([
'username1',
'username2',
'username3'
]);
};
-301
Ver Arquivo
@@ -1,301 +0,0 @@
/*
* Daemon.node
*** A node.JS addon that allows creating Unix/Linux Daemons in pure Javascript.
*** Copyright 2010 (c) <arthur@norgic.com>
* Under MIT License. See LICENSE file.
*/
#include <node/node.h>
#include <v8.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define PID_MAXLEN 10
using namespace v8;
// Go through special routines to become a daemon.
// if successful, returns daemon's PID
Handle<Value> Start(const Arguments& args) {
pid_t pid;
pid = fork();
if(pid > 0) exit(0);
if(pid < 0) exit(1);
// Can be changed after with process.umaks
umask(0);
setsid();
// Can be changed with process.chdir
chdir("/");
return Integer::New(getpid());
}
// Close Standard IN/OUT/ERR Streams
Handle<Value> CloseIO(const Arguments& args) {
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
}
// File-lock to make sure that only one instance of daemon is running.. also for storing PID
/* lock ( filename )
*** filename: a path to a lock-file.
*** Note: if filename doesn't exist, it will be created when function is called.
*/
Handle<Value> LockD(const Arguments& args) {
if(!args[0]->IsString())
return Boolean::New(false);
String::Utf8Value data(args[0]->ToString());
char pid_str[PID_MAXLEN+1];
int lfp = open(*data, O_RDWR | O_CREAT, 0640);
if(lfp < 0) exit(1);
if(lockf(lfp, F_TLOCK, 0) < 0) exit(0);
int len = snprintf(pid_str, PID_MAXLEN, "%d", getpid());
write(lfp, pid_str, len);
return Boolean::New(true);
}
class StreamPtr : public node::ObjectWrap
{
public:
explicit StreamPtr(FILE** fpp, const char *pmode);
~StreamPtr();
static Handle<Value> Open(const Arguments& args);
static Handle<Value> Close(const Arguments& args);
static Handle<Value> Redirect(const Arguments& args);
static void Initialize(Handle<Object> target);
static Handle<Value> New(const Arguments& args);
FILE** stream;
const char *mode;
private:
static Persistent<FunctionTemplate> constructor_template;
};
Persistent<FunctionTemplate> StreamPtr::constructor_template;
StreamPtr::StreamPtr(FILE** fpp, const char *pmode)
: stream(fpp)
, mode( pmode )
{
}
StreamPtr::~StreamPtr()
{
fclose(*stream);
}
Handle<Value> StreamPtr::Open(const Arguments& args)
{
HandleScope scope;
StreamPtr *fp = ObjectWrap::Unwrap<StreamPtr>(args.This());
char *new_file = *String::Utf8Value(args[0]->ToString());
*fp->stream = fopen(new_file, fp->mode);
// return if the creation of the new FILE* was successful;
return Boolean::New( *fp->stream != NULL );
}
Handle<Value> StreamPtr::Close(const Arguments& args)
{
HandleScope handle_scope;
StreamPtr *fp = ObjectWrap::Unwrap<StreamPtr>(args.This());
assert( fp && "object had no InternalField" );
int ret = fclose(*fp->stream);
return Boolean::New(ret == 0);
}
Handle<Value> StreamPtr::Redirect(const Arguments& args)
{
HandleScope scope;
if (Close(args)->ToBoolean()->Value())
{
return Open(args);
} else {
return Boolean::New(false);
}
}
Persistent<Object> stdin_obj;
Persistent<Object> stdout_obj;
Persistent<Object> stderr_obj;
void StreamPtr::Initialize(Handle<Object> target)
{
HandleScope scope;
Local<FunctionTemplate> file_pointer = FunctionTemplate::New(StreamPtr::New);
constructor_template = Persistent<FunctionTemplate>::New(file_pointer);
constructor_template->InstanceTemplate()->SetInternalFieldCount(1);
NODE_SET_PROTOTYPE_METHOD(constructor_template, "open", StreamPtr::Open);
NODE_SET_PROTOTYPE_METHOD(constructor_template, "close", StreamPtr::Close);
NODE_SET_PROTOTYPE_METHOD(constructor_template, "sendTo", StreamPtr::Redirect);
// Although we could expose the prototype directly, I see no reason to, as it's not
// very useful for anything other than the intended purpose.
//target->Set(String::NewSymbol("StreamPtr"), constructor_template->GetFunction());
stdin_obj = Persistent<Object>::New( constructor_template->GetFunction()->NewInstance() );
Handle<External> stdin_ptr = External::New( new StreamPtr( &stdin, "r" ) );
stdin_obj->SetInternalField(0, stdin_ptr);
target->Set(String::NewSymbol("stdin"), stdin_obj);
stdout_obj = Persistent<Object>::New( constructor_template->GetFunction()->NewInstance() );
Handle<External> stdout_ptr = External::New( new StreamPtr( &stdout, "w" ) );
stdout_obj->SetInternalField(0, stdout_ptr);
target->Set(String::NewSymbol("stdout"), stdout_obj);
stderr_obj = Persistent<Object>::New( constructor_template->GetFunction()->NewInstance() );
Handle<External> stderr_ptr = External::New( new StreamPtr( &stderr, "w" ) );
stderr_obj->SetInternalField(0, stderr_ptr);
target->Set(String::NewSymbol("stderr"), stderr_obj);
}
Handle<Value> StreamPtr::New(const Arguments& args)
{
HandleScope scope;
return scope.Close(args.This());
}
/* The default object:
* {
* "fork": true,
* "lock": "daemon.pid"
* "stdout": null,
* "stderr": null,
* "stdin": null,
* "umask": 0,
* "chroot": null,
* "chdir": ".",
* }
*/
#define SET_DEFAULT(obj, name, value) \
do { \
Local<String> str_##name = String::New(#name); \
if( !obj->Has( str_##name ) ) \
obj->Set( str_##name, (value) ); \
} while (0)
static inline void setDefaults(Handle<Object> &arg)
{
HandleScope scope;
SET_DEFAULT( arg, fork, Boolean::New(true) );
SET_DEFAULT( arg, lock, String::New("daemon.pid") );
SET_DEFAULT( arg, stdin, Null() );
SET_DEFAULT( arg, stdout, Null() );
SET_DEFAULT( arg, stderr, Null() );
SET_DEFAULT( arg, umask, Integer::New(0) );
SET_DEFAULT( arg, chroot, Null() );
SET_DEFAULT( arg, chdir, String::New(".") );
//SET_DEFAULT( arg, close_fds, Boolean::New(false) );
//SET_DEFAULT( arg, catch_signals, Boolean::New(false) );
}
#undef SET_DEFAULT
Handle<Value> Init(const Arguments& args)
{
HandleScope scope;
Local<Object> arg = args[0]->ToObject();
setDefaults( arg );
pid_t pid = 0;
if( arg->Get( String::New("fork") )->IsTrue() )
{
pid = fork();
if(pid > 0) exit(0);
if(pid < 0) exit(1);
}
if( !arg->Get( String::New("lock") )->IsNull() )
{
Local<String> file = arg->Get( String::New( "lock" ) )->ToString();
String::Utf8Value data(file);
char pid_str[PID_MAXLEN+1];
int lfp = open(*data, O_RDWR | O_CREAT, 0640);
if(lfp < 0) exit(1);
if(lockf(lfp, F_TLOCK, 0) < 0) exit(0);
int len = snprintf(pid_str, PID_MAXLEN, "%d", getpid());
write(lfp, pid_str, len);
}
if( arg->Get( String::New("stdin") )->IsNull() )
{
Local<Function> close = Function::Cast( *stdin_obj->Get( String::New("close") ) );
close->Call( stdin_obj, 0, NULL );
} else {
Local<Function> send_to = Function::Cast( *stdin_obj->Get( String::New("sendTo") ) );
Local<Value> string_arg = arg->Get( String::New("stdin") );
send_to->Call( stdin_obj, 1, &string_arg );
}
if( arg->Get( String::New("stdout") )->IsNull() )
{
Local<Function> close = Function::Cast( *stdout_obj->Get( String::New("close") ) );
close->Call( stdout_obj, 0, NULL );
} else {
Local<Function> send_to = Function::Cast( *stdout_obj->Get( String::New("sendTo") ) );
Local<Value> string_arg = arg->Get( String::New("stdout") );
send_to->Call( stdout_obj, 1, &string_arg );
}
if( arg->Get( String::New("stderr") )->IsNull() )
{
Local<Function> close = Function::Cast( *stderr_obj->Get( String::New("close") ) );
close->Call( stderr_obj, 0, NULL );
} else {
Local<Function> send_to = Function::Cast( *stderr_obj->Get( String::New("sendTo") ) );
Local<Value> string_arg = arg->Get( String::New("stderr") );
send_to->Call( stderr_obj, 1, &string_arg );
}
Local<Integer> umask_arg = arg->Get( String::New("umask") )->ToInteger();
umask( umask_arg->Value() );
if( !arg->Get( String::New("chroot") )->IsNull() )
{
char* dir = *String::AsciiValue( arg->Get( String::New("chroot") ) );
chroot( dir );
}
if( !arg->Get( String::New("chdir") )->IsNull() )
{
char* dir = *String::AsciiValue( arg->Get( String::New("chdir") ) );
chdir( dir );
}
return Integer::New(pid);
}
extern "C" void init(Handle<Object> target) {
HandleScope scope;
target->Set(String::New("start"), FunctionTemplate::New(Start)->GetFunction());
target->Set(String::New("lock"), FunctionTemplate::New(LockD)->GetFunction());
target->Set(String::New("closeIO"), FunctionTemplate::New(CloseIO)->GetFunction());
target->Set(String::New("init"), FunctionTemplate::New(Init)->GetFunction());
StreamPtr::Initialize(target);
}
-15
Ver Arquivo
@@ -1,15 +0,0 @@
srcdir = "."
blddir = "build"
VERSION = "0.0.1"
def set_options(opt):
opt.tool_options("compiler_cxx")
def configure(conf):
conf.check_tool("compiler_cxx")
conf.check_tool("node_addon")
def build(bld):
obj = bld.new_task_gen("cxx", "shlib", "node_addon")
obj.target = "daemon"
obj.source = "daemon.cc"
-88
Ver Arquivo
@@ -1,88 +0,0 @@
var sys = require('sys');
var Package = function() {};
Package.prototype._sanitize = function(content) {
// strip HTML
return content.replace(/<(.|\n)*?>/g, '');
};
var Error = exports.Error = function(error) {
this.error = error;
};
sys.inherits(Error, Package);
Error.prototype.toJSON = function() {
return {
type: 'error',
error: this.error
};
};
var Success = exports.Success = function(success) {
this.success = success;
};
sys.inherits(Success, Package);
Success.prototype.toJSON = function() {
return {
type: 'success',
success: this.success
};
};
var Message = exports.Message = function(from, body) {
this.from = from;
this.body = body;
};
sys.inherits(Message, Package);
Message.prototype.toJSON = function() {
return {
type: 'message',
user: this.from,
body: this._sanitize(this.body)
};
};
var Notice = exports.Notice = function(username, info) {
this.username = username;
this.info = info;
};
sys.inherits(Notice, Package);
Notice.prototype.toJSON = function() {
return {
type: 'notice',
user: this.username,
info: this.info
};
};
exports.TYPING_STATES = ['typing+', 'typing~', 'typing-'];
exports.STATUSES = ['available', 'away', 'idle'];
var Status = exports.Status = function(username, status, message) {
var statuses = exports.STATUSES + exports.TYPING_STATES;
this.username = username;
this.status = -~statuses.indexOf(status) ? status : statuses[0];
this.message = message;
};
sys.inherits(Status, Package);
Status.prototype.toJSON = function() {
return {
type: 'status',
user: this.username,
status: this.status,
message: this._sanitize(this.message || '')
};
};
var Offline = exports.Offline = function(username) {
this.username = username;
};
sys.inherits(Offline, Package);
Offline.prototype.toJSON = function() {
// A special type of status
return {
type: 'status',
user: this.username,
status: 'offline',
message: ''
};
};
Submodule server/libs/socket.io deleted from fbb9a46ba0
+154
Ver Arquivo
@@ -0,0 +1,154 @@
var MessageHandler = function(auth_handler, session_store) {
this.auth_handler = auth_handler;
this.session_store = session_store;
};
MessageHandler.prototype._auth = function(client, message) {
var session = this.session_store.get('identifier', message.identifier);
if(session) {
this.session_store.touch(session, client);
client.send({
type: 'AUTH',
loggedin: true
});
} else {
var auth_handler = this.auth_handler,
session_store = this.session_store;
// authenticate the new user
auth_handler.authenticate(client, message, function(res) {
// authentication succeeded, setup user
if(res.authenticated) {
// get user's friends
auth_handler.friends(client, res, function(friends_array) {
var friends_list = {},
sess,
indentifier;
for(var i = 0, fl = friends_array.length; i < fl; i++) {
sess = session_store.get('username', friends_array[i])
if(sess) {
// notify friends that user has logged on
sess.client.send({
type: 'STATUS',
username: res.username,
status: 'online'
});
friends_list[friends_array[i]] = sess.status;
} else {
friends_list[friends_array[i]] = 'offline';
}
}
identifier = session_store.create(res.username,
client,
friends_array
);
// notify user that they're logged on
client.send({
type: 'AUTH',
loggedin: true,
friends: friends_dict
});
});
} else {
// authentication failed
client.send({
type: 'AUTH',
loggedin: false
});
}
});
}
}
MessageHandler.prototype._im = function(client, to, message) {
var sender = this.session_handler.get('client', client),
recipient;
if(session) {
recipient = this.session_handler.get('username', to);
if(recipient) {
recipient.client.send({
type: 'IM',
from: sender.username,
message: message
});
} else {
client.send({
type: 'ERROR',
origin: 'IM',
to: to
});
}
}
};
MessageHandler.prototype._status = function(client, status, status_msg) {
var session = this.session_handler.get('client', client),
friend;
if(['online', 'away', 'offline'].indexOf(status) != -1) {
session.status = status;
this.session_store.set(session.username, session);
// let user's friends know about the new status
for(var i = 0, fl = session.friends.length; i < fl; i++) {
friend = this.session_store.get('username', session.friends[i]);
if(friend) {
friend.client.send({
type: 'STATUS',
username: session.username,
status: status
});
}
}
}
};
MessageHandler.prototype._disconnect = function(client, SESSION_TIMEOUT) {
var threshold = +new Date - SESSION_TIMEOUT,
session = this.session_store.get('client', client);
if(session && session.lastAccess < threshold) {
if(session.status != 'offline')
this._status(session.username, 'offline');
this.session_store.remove(session.username);
}
};
MessageHandler.prototype.message = function(client, message) {
switch(message.type) {
case 'AUTH':
this._auth(client, message);
break;
case 'IM':
this._im(client, message.to, message.message);
break;
case 'STATUS':
this._status(client, message.status, message.status_msg);
break;
}
};
MessageHandler.prototype.disconnect = function(client, SESSION_TIMEOUT) {
var self = this;
setTimeout(function() {
self._disconnect(client, SESSION_TIMEOUT);
}, SESSION_TIMEOUT);
};
module.exports = function createInstance(auth_handler, session_store) {
return new MessageHandler(auth_handler, session_store);
};
-67
Ver Arquivo
@@ -1,67 +0,0 @@
var url = require('url'),
Hub = require('./im/hub');
module.exports = function setupHub(options) {
options = options || {};
store = new Hub(options);
return function session(req, res, next) {
req.sessionStore = store;
req.sessionID = req.cookies[options.authentication.cookie];
if(!req.cookies) {
next(new Error('session requires cookieDecoder to work properly'));
return;
}
if(req.dev) {
next();
return;
}
if(req.sessionID) {
store.get(req, function(err, sess) {
if(err) {
next(err);
return;
}
if(!sess) {
next(new Error(JSON.stringify({
type: 'error',
error: 'not authenticated'})));
return;
}
sess.touch();
if(url.parse(req.url).pathname === '/listen') {
req.connection.setTimeout(5 * 60 * 1000);
sess.listener(res);
store.set(req.sessionID, sess);
if(msg = sess.message_queue.shift())
sess._send.apply(sess, msg);
} else {
sess.connection = res;
}
req.session = sess;
res.find = store.find.bind(store);
res.message = function(to, package) {
store.message(req.session, to, package);
};
res.status = function(value, message) {
req.session.status(value, message);
};
res.signOff = function() { store.signOff(req.sessionID); };
next();
});
} else {
next(new Error(JSON.stringify({
type: 'error',
error: 'not authenticated'})));
}
};
};
-126
Ver Arquivo
@@ -1,126 +0,0 @@
var events = require('events'),
sys = require('sys'),
packages = require('../../libs/packages'),
o_ = require('../../libs/utils'),
User = require('./user');
var Hub = module.exports = function Hub(options) {
this.events = new events.EventEmitter();
this.auth = options.authentication;
this.sessions = {};
this.maxAge = options.maxAge || 4 * 60 * 60 * 1000;
this.reapInterval = options.reapInterval || 60 * 1000;
if(this.reapInterval !== -1) {
setInterval(function(self) {
self.reap(self.maxAge);
}, this.reapInterval, this);
}
this.events.addListener('update', o_.bind(function(package) {
var _package = package.toJSON();
if(package.type == 'status' && package.status == 'offline') {
var sids = Object.keys(this.sessions), sid, sess;
for(sid in this.sessions) {
sess = this.sessions[sid];
if(sess.data('username') == package.username) {
if(sess.listeners.length)
sess.send(200, {type: 'goodbye'});
delete this.sessions[sid];
break;
}
}
}
}, this));
};
Hub.prototype.destroy = function(sid, fn) {
this.set(sid, null, fn);
};
Hub.prototype.reap = function(ms) {
var threshold = +new Date - ms,
sids = Object.keys(this.sessions);
for(var i = 0, len = sids.length; i < len; ++i) {
var sid = sids[i], sess = this.sessions[sid];
if(sess.lastAccess < threshold) {
this.events.emit('update', new packages.Offline(sess.data('username')));
}
}
};
Hub.prototype.get = function(req, fn) {
if(this.sessions[req.sessionID]) {
fn(null, this.sessions[req.sessionID]);
} else {
this.auth.authenticate(req, o_.bind(function(data) {
if(data) {
var session = new User(req.sessionID, data);
this.set(req.sessionID, session);
this.auth.friends(req, data, o_.bind(function(friends) {
var friends_copy = friends.slice();
o_.values(this.sessions).filter(function(friend) {
return ~friends.indexOf(friend.data('username'));
}).forEach(function(friend) {
var username = friend.data('username');
friends_copy[friends_copy.indexOf(username)] =
[username, friend.status()];
}, this);
session._friends(friends_copy);
session.events.addListener('status',
o_.bind(function(value, message) {
this.events.emit(
'update',
new packages.Status(session.data('username'),
value,
message)
);
}, this));
this.events.addListener('update',
o_.bind(session.receivedUpdate, session));
this.set(req.sessionID, session);
fn(null, session);
}, this));
} else {
fn();
}
}, this));
}
};
Hub.prototype.set = function(sid, sess, fn) {
this.sessions[sid] = sess;
fn && fn();
};
Hub.prototype.find = function(username, fn) {
for(var sid in this.sessions) {
var session = this.sessions[sid],
sess_username = session.data('username');
if(sess_username == username) {
fn(session);
return;
}
}
fn(false);
};
Hub.prototype.message = function(from, to, package) {
try {
package.user = from;
to.send(package);
from.respond(new packages.Success('sent'));
} catch(e) {
from.respond(new packages.Error(e.description));
}
};
Hub.prototype.signOff = function(sid) {
if(sid in this.sessions)
this.events.emit('update',
new packages.Offline(
this.sessions[sid].data('username')));
};
-114
Ver Arquivo
@@ -1,114 +0,0 @@
var events = require('events'),
packages = require('../../libs/packages'),
o_ = require('../../libs/utils');
var User = module.exports = function(id, data) {
this.id = id;
this.connection = null;
this.listeners = [];
this.message_queue = [];
this.convos = {};
this._data = data;
this.events = new events.EventEmitter();
this.status(packages.STATUSES[0], '');
setInterval(o_.bind(this._expireConns, this), 500);
};
User.prototype.receivedUpdate = function(package) {
if(this.friends.indexOf(package.username))
this.send(package);
};
User.prototype._friends = function(friends) {
this.friends = friends;
this.send(JSON.stringify({
type: 'hello',
username: this.data('username'),
friends: friends
}));
};
User.prototype._expireConns = function() {
var conn,
noop = JSON.stringify({type: 'noop'}),
noop_headers = {
'Content-Type': 'application/json',
'Content-Length': noop.length
};
for(var i = 0; i < this.listeners.length; i++) {
conn = this.listeners[i].connection;
if((Date.now() - conn._idleStart) >= conn._idleTimeout - 2000) {
this.listeners[i].writeHead(200, noop_headers);
this.listeners[i].end(noop);
this.listeners.splice(i, 1);
i--;
}
}
};
User.prototype.listener = function(conn) {
this.listeners.push(conn);
};
User.prototype.respond = function(code, message, callback) {
this._send('connection', code, message, callback);
};
User.prototype.send = function(code, message, callback) {
this._send('listener', code, message, callback);
};
User.prototype._send = function(type, code, message, callback) {
if(!message && typeof code != 'number') {
callback = message;
message = code;
code = 200;
}
if(typeof message != 'string')
message = JSON.stringify(message);
if(type == 'connection' && this.connection) {
this.connection.writeHead(code || 200, {
'Content-Type': 'application/json',
'Content-Length': message.length
});
this.connection.end(message);
} else {
if(!this.listeners.length)
return this.message_queue.push(arguments);
var cx = this.listeners.slice(), conn;
this.listeners = [];
while(conn = cx.shift()) {
conn.writeHead(code || 200, {
'Content-Type': 'application/json',
'Content-Length': message.length
});
conn.end(message);
}
if(callback) callback();
}
};
User.prototype.data = function(key, def) {
if(key == 'id') return this.id;
return this._data[key] || this['_' + key] ||
(typeof this[key] != 'function' && this[key]) ||
def || false;
};
User.prototype.touch = function() {
this.lastAccess = +new Date;
};
User.prototype.status = function(value, message) {
if(!value)
return this._status;
this._status = value;
this._status_message = message;
this.events.emit('status', value, message);
};
Ver Arquivo
+90
Ver Arquivo
@@ -0,0 +1,90 @@
var crypto = require('crypto');
var Session = function(username) {
this.username = username;
this.authenticated = false;
this.lastAccess = new Date().getTime();
this.client = -1;
this.identifier = -1;
this.friends = [];
this.status = 'online';
};
var MemorySessionStore = function() {
this.sessions = {};
this.sessionClientMap = {};
this.sessionIdentifierMap = {};
};
MemorySessionStore.prototype.create = function(username, client, friends) {
var session = new Session(username);
session.client = client;
if(friends) {
for(var i = 0, fl = friends.length; i < fl; i++)
session.push(friends[i])
}
this.sessions[username] = session;
this.sessionClientMap[client.sessionId] = username;
// create a re-authentication identifier
var md5 = crypto.createHash('md5'),
identifier = md5.update(username + client.sessionId).digest('hex');
this.sessionIdentifierMap[identifier] = username;
session.identifier = identifier;
return identifier;
};
MemorySessionStore.prototype.set = function(username, session) {
this.sessions[username] = session;
this.sessionClientMap[session.client.sessionId] = username;
this.sessionIdentifierMap[session.identifier] = username;
};
MemorySessionStore.prototype.touch = function(session, client) {
var new_session = session;
new_session.lastAccess = new Date().getTime();
new_session.client = client;
this.remove(new_session.username);
this.set(new_session.username, new_session);
};
MemorySessionStore.prototype.get = function(key, val) {
switch(key) {
case 'client':
return this.sessions[this.sessionClientMap[val]];
break;
case 'username':
return this.sessions[val];
break;
case 'identifier':
return this.sessions[this.sessionIdentifierMap[val]];
break;
}
return null;
}
MemorySessionStore.prototype.all = function() {
return this.sessions;
};
MemorySessionStore.prototype.remove = function(username) {
if(this.session.username) {
var clientid = this.sessions[username].client.sessionId,
identifier = this.sessions[username].identifier;
delete this.sessionClientMap[clientid];
delete this.sessionIdentifierMap[identifier];
delete this.sessions[username];
}
};
var instance = new MemorySessionStore();
module.exports = function getInstance() {
return instance;
};
+10 -6
Ver Arquivo
@@ -7,18 +7,22 @@
// === Host and Port ===
//
// Define the host and port that Ajax IM will run on.
//
// Note: Setting APP_HOST to null will run the server on port 8000 for any
// hostname!
APP_HOST = 'localhost';
APP_PORT = 8000;
// === Authentication Library ===
//
// This is the library (from libs/authenticate/) that we will use to
// authenticate a user signing in. The value should be the name of the file
// without the '.js' part. 'index' is the default library.
// Document me!
AUTH_LIBRARY = 'default';
SESSION_STORE = 'memory';
MESSAGE_HANDLER = 'default';
// === Daemon ===
//
// Define where the PID and log files will be deposited when run as a daemon.
/*
// Broken! Removed for the time being.
PID_FILE = '/tmp/ajaxim.pid';
LOG_FILE = '/var/run/ajaxim.log';
LOG_FILE = '/var/run/ajaxim.log';
*/