thywill
Version:
A Node.js clustered framework for single page web applications based on asynchronous messaging.
323 lines (293 loc) • 10.2 kB
JavaScript
/**
* @fileOverview
* SocketIoExpressClientInterface class definition.
*/
var util = require('util');
var async = require('async');
var clone = require('clone');
var Thywill = require('thywill');
var SocketIoClientInterface = require('./socketIoClientInterface');
// -----------------------------------------------------------
// Class Definition
// -----------------------------------------------------------
/**
* @class
* An extension of SocketIoClientInterface that uses Express to serve files
* and manage sessions.
*
* @see SocketIoClientInterface
*/
function SocketIoExpressClientInterface () {
SocketIoExpressClientInterface.super_.call(this);
}
util.inherits(SocketIoExpressClientInterface, SocketIoClientInterface);
var p = SocketIoExpressClientInterface.prototype;
// -----------------------------------------------------------
// 'Static' parameters
// -----------------------------------------------------------
SocketIoExpressClientInterface.CONFIG_TEMPLATE = clone(SocketIoClientInterface.CONFIG_TEMPLATE);
SocketIoExpressClientInterface.CONFIG_TEMPLATE.server.app = {
_configInfo: {
description: 'An Express application instance.',
types: 'function',
required: false
}
};
SocketIoExpressClientInterface.CONFIG_TEMPLATE.sessions = {
store: {
_configInfo: {
description: 'For Express sessions: the session store instance.',
types: 'object',
required: false
}
},
cookieKey: {
_configInfo: {
description: 'For Express sessions: The name used for session cookies.',
types: 'string',
required: false
}
},
cookieSecret: {
_configInfo: {
description: 'For Express sessions: The secret value used for session cookies.',
types: 'string',
required: false
}
}
};
// -----------------------------------------------------------
// Initialization
// -----------------------------------------------------------
/**
* @see SocketIoClientInterface#_startup
*/
p._startup = function (callback) {
this._setExpressToServeResources();
this._initializeSocketIo();
this._attachExpressSessionsToSockets();
this._setupBootstrapResources(callback);
};
/**
* Add middleware to Express to ensure that it will serve Resources regardless
* of current or future addition of routes or middleware.
*
* This involves being a little devious.
*/
p._setExpressToServeResources = function () {
var self = this;
var express = require('express');
// This middleware essentially performs the action of a route. This is a way
// to ensure that Thywill routes will run no matter the order in which
// routes or middleware are added.
var middleware = function (req, res, next) {
// Is this request in the right base path?
if (req.path.match(self.config.baseClientPathRegExp)) {
// But do we have a resource for this?
self.getResource(req.path, function (error, resource) {
// If there's an error or a resource, handle it.
if (error || resource) {
self.handleResourceRequest(req, res, next, error, resource);
} else {
// Otherwise, onwards and let Express take over.
next();
}
});
} else {
// Otherwise, onwards and let Express take over.
next();
}
};
// Add the middleware.
middleware.isThywill = true;
this.config.server.app.use(middleware);
/**
* Put the Thywill middleware in the right place in the Express stack,
* which should be last before the router. At the time this is called,
* the router middleware may or may not have been added, and other
* middleware may also later be added.
*/
function positionMiddleware () {
// The stack wil be at least length 1, as the Thywill middleware has been put there
// before this function is called.
var desiredIndex = self.config.server.app.stack.length - 1;
// Get the index of the router. Should be last, but if we can add
// middleware out of sequence, so can other people.
for (var index = 0; index < self.config.server.app.stack.length; index++) {
// TODO: a less sketchy way of identifying the router middleware.
if (self.config.server.app.stack[index].handle === self.config.server.app._router.middleware) {
// If the router middleware is added, then routes have been
// added, and thus no more middleware will be added.
clearInterval(positionMiddlewareIntervalId);
desiredIndex = index;
break;
}
}
// If it is already in the right place, then we're good.
if(self.config.server.app.stack[desiredIndex].handle.isThywill) {
return;
}
// Otherwise, remove from the present position.
var stackItem;
self.config.server.app.stack = self.config.server.app.stack.filter(function (element, index, array) {
if (element.handle.isThywill) {
stackItem = element;
}
return !element.handle.isThywill;
});
// Insert it into the relevant place.
self.config.server.app.stack.splice(desiredIndex, 0, stackItem);
}
// This will run until the Router middleware is added, which happens
// when the first route is set.
var positionMiddlewareIntervalId = setInterval(positionMiddleware, 100);
};
/**
* This is how we attach Express sessions to the sockets. The result is that
* socket.handshake.session holds the session, and socket.handshake.sessionId
* has the session ID.
*
* This code was adapted from:
* https://github.com/alphapeter/socket.io-express
*/
p._attachExpressSessionsToSockets = function () {
var self = this;
var express = require('express');
var cookieParser = express.cookieParser(this.config.sessions.cookieSecret);
// We're using the authorization hook, but there is no authorizing going on
// here - we're only attaching the session to the socket handshake.
this.socketFactory.set('authorization', function (data, callback) {
if (data && data.headers && data.headers.cookie) {
cookieParser(data, {}, function (error) {
if (error) {
self.thywill.log.debug(error);
callback('COOKIE_PARSE_ERROR', false);
return;
}
var sessionId = data.signedCookies[self.config.sessions.cookieKey];
self.loadSession(sessionId, function (error, session) {
// Add the sessionId. This will show up in
// socket.handshake.sessionId.
//
// It's useful to set the ID and session separately because of
// those fun times when you have an ID but no session - it makes
// debugging that much easier.
data.sessionId = sessionId;
if (error) {
self.thywill.log.debug(error);
callback('ERROR', false);
} else if (!session) {
callback('NO_SESSION', false);
} else {
// Add the session. This will show up in
// socket.handshake.session.
data.session = session;
callback(null, true);
}
});
});
} else {
callback('NO_COOKIE', false);
}
});
};
// -----------------------------------------------------------
// Connection and serving data methods
// -----------------------------------------------------------
/**
* Extract the session data we're going to be using from the socket.
*
* @param {Object} socket
* A socket.
* @param {Object}
* Of the form {connectionId: string, sessionId: string, session: object}.
* Either sessionId or session can be undefined under some circumstances,
* so check.
*/
p.connectionDataFromSocket = function (socket) {
var data = {
connectionId: socket.id,
sessionId: undefined,
session: undefined
};
if (socket.handshake) {
data.sessionId = socket.handshake.sessionId;
data.session = socket.handshake.session;
}
return data;
};
/**
* Handle a request with http.Server or Express.
*
* @param {Object} req
* Request object from the server.
* @param {Object} res
* Response object from the server.
* @param {function} next
* Express callback function.
* @param {mixed} error
* Any error resulting from finding the resource.
* @param {Resource} resource
* The resource to server up.
*/
p.handleResourceRequest = function (req, res, next, error, resource) {
var self = this;
if (error) {
this._send500ResourceResponse(req, res, next, error);
return;
} else if (!resource) {
this._send500ResourceResponse(req, res, next, new Error('Missing resource.'));
return;
}
if (resource.isInMemory()) {
res.setHeader('Content-Type', resource.type);
res.send(resource.buffer);
} else if (resource.isPiped()) {
res.setHeader('Content-Type', resource.type);
res.sendFile(resource.filePath, {
// TODO: maxage configuration.
maxAge: 0
});
} else {
// This resource is probably not set up correctly. It should either have
// data in memory or be set up to be piped.
this._send500ResourceResponse(req, res, next, new Error('Resource incorrectly configured: ' + req.path));
}
};
/**
* Send a 500 error response down to the client when a resource is requested.
*
* @param {Object} req
* Request object from the server.
* @param {Object} res
* Response object from the server.
* @param {function} next
* Express callback function.
* @param {mixed} error
* The error.
*/
p._send500ResourceResponse = function (req, res, next, error) {
this.thywill.log.error(error);
next(error.stack || error.toString());
};
/**
* @see ClientInterface#loadSession
*
* Using Express sessions.
*/
p.loadSession = function (client, callback) {
var sessionId = this._clientOrSessionIdToSessionId(client);
this.config.sessions.store.load(sessionId, callback);
};
/**
* @see ClientInterface#storeSession
*
* Using Express sessions.
*/
p.storeSession = function (client, session, callback) {
session.save(callback);
};
// -----------------------------------------------------------
// Exports - Class Constructor
// -----------------------------------------------------------
module.exports = SocketIoExpressClientInterface;