3 /*jshint node:true, indent:2, curly:false, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true,
4 regexp:true, undef:true, strict:true, trailing:true, white:true */
5 /*global X:true, Backbone:true, _:true, XM:true, XT:true, SYS:true, jsonpatch:true*/
6 process.chdir(__dirname);
8 Backbone = require("backbone");
9 _ = require("underscore");
10 jsonpatch = require("json-patch");
13 var express = require('express');
19 var options = require("./lib/options"),
21 schemaSessionOptions = {},
22 privSessionOptions = {};
25 * Include the X framework.
29 // Loop through files and load the dependencies.
30 // Apes the enyo package process
31 // TODO: it would be nice to use a more standardized way
32 // of loading our libraries (tools and backbone-x) here
34 X.relativeDependsPath = "";
35 X.depends = function () {
36 var dir = X.relativeDependsPath,
37 files = X.$A(arguments),
40 _.each(files, function (file) {
41 if (X.fs.statSync(X.path.join(dir, file)).isDirectory()) {
42 pathBeforeRecursion = X.relativeDependsPath;
43 X.relativeDependsPath = X.path.join(dir, file);
44 X.depends("package.js");
45 X.relativeDependsPath = pathBeforeRecursion;
47 require(X.path.join(dir, file));
53 // Load other xTuple libraries using X.depends above.
54 require("backbone-relational");
55 X.relativeDependsPath = X.path.join(X.basePath, "../lib/tools/source");
56 require("../lib/tools");
57 X.relativeDependsPath = X.path.join(X.basePath, "../lib/backbone-x/source");
58 require("../lib/backbone-x");
61 // Argh!!! Hack because `XT` has it's own string format function that
62 // is incompatible with `X`....
63 String.prototype.f = function () {
64 return X.String.format.apply(this, arguments);
67 // Another hack: quiet the logs here.
68 XT.log = function () {};
73 // load some more required files
74 var datasource = require("./lib/ext/datasource");
75 require("./lib/ext/models");
76 require("./lib/ext/smtp_transport");
78 datasource.setupPgListeners(X.options.datasource.databases, {
79 email: X.smtpTransport.sendMail
82 // load the encryption key, or create it if it doesn't exist
83 // it should created just once, the very first time the datasoruce starts
84 var encryptionKeyFilename = X.options.datasource.encryptionKeyFile || './lib/private/encryption_key.txt';
85 X.fs.exists(encryptionKeyFilename, function (exists) {
87 X.options.encryptionKey = X.fs.readFileSync(encryptionKeyFilename, "utf8");
89 X.options.encryptionKey = Math.random().toString(36).slice(2);
90 X.fs.writeFile(encryptionKeyFilename, X.options.encryptionKey);
95 XT.session = Object.create(XT.Session);
96 XT.session.schemas.SYS = false;
98 var getExtensionDir = function (extension) {
100 "/private-extensions": X.path.join(__dirname, "../..", extension.location, "source", extension.name),
101 "/xtuple-extensions": X.path.join(__dirname, "../..", extension.location, "source", extension.name),
102 "/core-extensions": X.path.join(__dirname, "../enyo-client/extensions/source", extension.name),
103 "npm": X.path.join(__dirname, "../node_modules", extension.name)
105 return dirMap[extension.location];
107 var useClientDir = function (path, dir) {
108 path = path.indexOf("npm") === 0 ? "/" + path : path;
109 _.each(X.options.datasource.databases, function (orgValue, orgKey, orgList) {
110 app.use("/" + orgValue + path, express.static(dir, { maxAge: 86400000 }));
113 var loadExtensionClientside = function (extension) {
114 var extensionLocation = extension.location === "npm" ? extension.location : extension.location + "/source";
115 useClientDir(extensionLocation + "/" + extension.name + "/client", X.path.join(getExtensionDir(extension), "client"));
117 var loadExtensionRoutes = function (extension) {
118 var manifest = JSON.parse(X.fs.readFileSync(X.path.join(getExtensionDir(extension),
119 "database/source/manifest.js")));
120 _.each(manifest.routes || [], function (routeDetails) {
121 var verb = (routeDetails.verb || "all").toLowerCase(),
122 func = require(X.path.join(getExtensionDir(extension),
123 "node-datasource", routeDetails.filename))[routeDetails.functionName];
125 if (_.contains(["all", "get", "post", "patch", "delete"], verb)) {
126 app[verb]('/:org/' + routeDetails.path, func);
128 console.log("Invalid verb for extension-defined route " + routeDetails.path);
133 schemaSessionOptions.username = X.options.databaseServer.user;
134 schemaSessionOptions.database = X.options.datasource.databases[0];
135 // XXX note that I'm not addressing an underlying bug that we don't wait to
136 // listen on the port until all the setup is done
137 schemaSessionOptions.success = function () {
141 var extensions = new SYS.ExtensionCollection();
143 database: X.options.datasource.databases[0],
144 success: function (coll, results, options) {
146 // XXX time bomb: assuming app has been initialized, below, by now
147 XT.log("Could not load extension routes or client-side code because the app has not started");
151 useClientDir("/client", "../enyo-client/application");
152 _.each(results, loadExtensionRoutes);
153 _.each(results, loadExtensionClientside);
157 XT.session.loadSessionObjects(XT.session.SCHEMA, schemaSessionOptions);
159 privSessionOptions.username = X.options.databaseServer.user;
160 privSessionOptions.database = X.options.datasource.databases[0];
161 XT.session.loadSessionObjects(XT.session.PRIVILEGES, privSessionOptions);
167 Grab the version number from the package.json file.
170 var packageJson = X.fs.readFileSync("../package.json");
172 X.version = JSON.parse(packageJson).version;
178 * Module dependencies.
180 var passport = require('passport'),
181 oauth2 = require('./oauth2/oauth2'),
182 routes = require('./routes/routes'),
183 socketio = require('socket.io'),
184 url = require('url'),
185 utils = require('./oauth2/utils'),
186 user = require('./oauth2/user'),
189 // TODO - for testing. remove...
190 //http://stackoverflow.com/questions/13091037/node-js-heap-snapshots-and-google-chrome-snapshot-viewer
191 //var heapdump = require("heapdump");
192 // Use it!: https://github.com/c4milo/node-webkit-agent
193 //var agent = require('webkit-devtools-agent');
196 * ###################################################
199 * Sometimes we need to change how an npm packages works.
200 * Don't edit the packages directly, override them here.
201 * ###################################################
205 Define our own authentication criteria for passport. Passport itself defines
206 its authentication function here:
207 https://github.com/jaredhanson/passport/blob/master/lib/passport/http/request.js#L74
208 We are stomping on that method with our own special business logic.
209 The ensureLoggedIn function will not need to be changed, because that calls this.
211 require('http').IncomingMessage.prototype.isAuthenticated = function () {
214 var creds = this.session.passport.user;
216 if (creds && creds.id && creds.username && creds.organization) {
219 destroySession(this.sessionID, this.session);
224 // Stomping on express/connect's Cookie.prototype to only update the expires property
225 // once a minute. Otherwise it's hit on every session check. This cuts down on chatter.
226 // See more details here: https://github.com/senchalabs/connect/issues/670
227 require('express/node_modules/connect/lib/middleware/session/cookie').prototype.__defineSetter__("expires", require('./stomps/expires').expires);
229 // Stomp on Express's cookie serialize() to not send an "expires" value to the browser.
230 // This makes the browser cooke a "session" cookie that will never expire and only
231 // gets removed when the user closes the browser. We still set express.session.cookie.maxAge
232 // below so our persisted session gets an expires value, but not the browser cookie.
233 // See this issue for more details: https://github.com/senchalabs/connect/issues/328
234 require('express/node_modules/cookie').serialize = require('./stomps/cookie').serialize;
236 // Stomp on Connect's session.
237 // https://github.com/senchalabs/connect/issues/641
238 function stompSessionLoad() {
240 return require('./stomps/session');
242 require('express/node_modules/connect').middleware.__defineGetter__('session', stompSessionLoad);
243 require('express/node_modules/connect').__defineGetter__('session', stompSessionLoad);
244 require('express').__defineGetter__('session', stompSessionLoad);
247 * ###################################################
248 * END Overrides section.
249 * ###################################################
257 sslOptions.key = X.fs.readFileSync(X.options.datasource.keyFile);
258 if (X.options.datasource.caFile) {
259 sslOptions.ca = _.map(X.options.datasource.caFile, function (obj) {
262 return X.fs.readFileSync(obj);
265 sslOptions.cert = X.fs.readFileSync(X.options.datasource.certFile);
268 * Express configuration.
272 var server = X.https.createServer(sslOptions, app),
273 parseSignedCookie = require('express/node_modules/connect').utils.parseSignedCookie,
274 //MemoryStore = express.session.MemoryStore,
275 XTPGStore = require('./oauth2/db/connect-xt-pg')(express),
277 //sessionStore = new MemoryStore(),
278 sessionStore = new XTPGStore({ hybridCache: X.options.datasource.requireCache || false }),
279 Session = require('express/node_modules/connect/lib/middleware/session').Session,
280 Cookie = require('express/node_modules/connect/lib/middleware/session/cookie'),
281 cookie = require('express/node_modules/cookie'),
282 privateSalt = X.fs.readFileSync(X.options.datasource.saltFile).toString() || 'somesecret';
284 // Conditionally load express.session(). REST API endpoints using OAuth tokens do not get sessions.
285 var conditionalExpressSession = function (req, res, next) {
290 // REST API endpoints start with "/api" in their path.
291 // The 'assets' folder and login page are sessionless.
292 if ((/^api/i).test(req.path.split("/")[2]) ||
293 (/^\/assets/i).test(req.path) ||
295 req.path === "/favicon.ico" ||
296 req.path === "/forgot-password" ||
297 req.path === "/recover") {
301 if (req.path === "/login") {
302 // TODO - Add check against X.options database array
303 key = req.body.database + ".sid";
304 } else if (req.path.split("/")[1]) {
305 key = req.path.split("/")[1] + ".sid";
307 // TODO - Dynamically name the cookie after the database.
308 console.log("### FIX ME ### setting cookie name to 'connect.sid' for path = ", JSON.stringify(req.path));
309 console.log("### FIX ME ### cookie name should match database name!!!");
310 console.trace("### At this location ###");
314 // Instead of doing app.use(express.session()) we call the package directly
315 // which returns a function (req, res, next) we can call to do the same thing.
316 var init_session = express.session({
320 // See cookie stomp above for more details on how this session cookie works.
325 maxAge: (X.options.datasource.sessionTimeout * 60 * 1000) || 3600000
327 sessionIDgen: function () {
328 // TODO: Stomp on connect's sessionID generate.
329 // https://github.com/senchalabs/connect/issues/641
330 return key.split(".")[0] + "." + utils.generateUUID();
334 init_session(req, res, next);
338 // Conditionally load passport.session(). REST API endpoints using OAuth tokens do not get sessions.
339 var conditionalPassportSession = function (req, res, next) {
342 // REST API endpoints start with "/api" in their path.
343 // The 'assets' folder and login page are sessionless.
344 if ((/^api/i).test(req.path.split("/")[2]) ||
345 (/^\/assets/i).test(req.path) ||
347 req.path === "/favicon.ico"
352 // Instead of doing app.use(passport.session())
353 var init_passportSessions = passport.session();
355 init_passportSessions(req, res, next);
359 app.configure(function () {
362 // gzip all static files served.
363 app.use(express.compress());
364 // Add a basic view engine that will render files from "views" directory.
365 app.set('view engine', 'ejs');
367 // TODO - This outputs access logs like apache2 and some other user things.
368 //app.use(express.logger());
370 app.use(express.cookieParser());
371 app.use(express.bodyParser());
373 // Conditionally load session packages. Based off these examples:
374 // http://stackoverflow.com/questions/9348505/avoiding-image-logging-in-express-js/9351428#9351428
375 // http://stackoverflow.com/questions/13516898/disable-csrf-validation-for-some-requests-on-express
376 app.use(conditionalExpressSession);
377 app.use(passport.initialize());
378 app.use(conditionalPassportSession);
381 app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
385 * Passport configuration.
387 require('./oauth2/passport');
390 * Setup HTTP routes and handlers.
394 app.use(express.favicon(__dirname + '/views/login/assets/favicon.ico'));
395 app.use('/assets', express.static('views/login/assets', { maxAge: 86400000 }));
397 app.get('/:org/dialog/authorize', oauth2.authorization);
398 app.post('/:org/dialog/authorize/decision', oauth2.decision);
399 app.post('/:org/oauth/token', oauth2.token);
401 app.get('/:org/discovery/v1alpha1/apis/v1alpha1/rest', routes.restDiscoveryGetRest);
402 app.get('/:org/discovery/v1alpha1/apis/:model/v1alpha1/rest', routes.restDiscoveryGetRest);
403 app.get('/:org/discovery/v1alpha1/apis', routes.restDiscoveryList);
405 app.get('/:org/api/userinfo', user.info);
407 app.post('/:org/api/v1alpha1/services/:service/:id', routes.restRouter);
408 app.all('/:org/api/v1alpha1/resources/:model/:id', routes.restRouter);
409 app.all('/:org/api/v1alpha1/resources/:model', routes.restRouter);
410 app.all('/:org/api/v1alpha1/resources/*', routes.restRouter);
412 app.get('/', routes.loginForm);
413 app.post('/login', routes.login);
414 app.get('/forgot-password', routes.forgotPassword);
415 app.post('/recover', routes.recoverPassword);
416 app.get('/:org/recover/reset/:id/:token', routes.verifyRecoverPassword);
417 app.post('/:org/recover/resetUpdate', routes.resetRecoveredPassword);
418 app.get('/login/scope', routes.scopeForm);
419 app.post('/login/scopeSubmit', routes.scope);
420 app.get('/logout', routes.logout);
421 app.get('/:org/logout', routes.logout);
422 app.get('/:org/app', routes.app);
423 app.get('/:org/debug', routes.debug);
425 app.all('/:org/credit-card', routes.creditCard);
426 app.all('/:org/change-password', routes.changePassword);
427 app.all('/:org/client/build/client-code', routes.clientCode);
428 app.all('/:org/email', routes.email);
429 app.all('/:org/export', routes.exxport);
430 app.get('/:org/file', routes.file);
431 app.get('/:org/generate-report', routes.generateReport);
432 app.all('/:org/install-extension', routes.installExtension);
433 app.get('/:org/locale', routes.locale);
434 app.all('/:org/oauth/generate-key', routes.generateOauthKey);
435 app.get('/:org/reset-password', routes.resetPassword);
436 app.post('/:org/oauth/revoke-token', routes.revokeOauthToken);
437 app.all('/:org/vcfExport', routes.vcfExport);
440 // Set up the other servers we run on different ports.
442 var redirectServer = express();
443 redirectServer.get(/.*/, routes.redirect); // RegEx for "everything"
444 redirectServer.listen(X.options.datasource.redirectPort, X.options.datasource.bindAddress);
447 * Start the express server. This is the NEW way.
449 // TODO - Active browser sessions can make calls to this server when it hasn't fully started.
450 // That can cause it to crash at startup.
451 // Need a way to get everything loaded BEFORE we start listening. Might just move this to the end...
452 io = socketio.listen(server.listen(X.options.datasource.port, X.options.datasource.bindAddress));
454 X.log("Server listening at: ", X.options.datasource.bindAddress);
455 X.log("node-datasource started on port: ", X.options.datasource.port);
456 X.log("redirectServer started on port: ", X.options.datasource.redirectPort);
457 X.log("Databases accessible from this server: \n", JSON.stringify(X.options.datasource.databases, null, 2));
461 * Destroy a single session.
462 * @param {Object} val - Session object.
463 * @param {String} key - Session id.
465 destroySession = function (key, val) {
471 if (val && val.socket && val.socket.id) {
472 _.each(io.sockets.sockets, function (sockVal, sockKey, sockList) {
473 if (val.socket.id === sockKey) {
474 _.each(sockVal.manager.namespaces, function (spaceVal, spaceKey, spaceList) {
475 sockVal.flags.endpoint = spaceVal.name;
476 // Tell the client it timed out. This will redirect the client to /logout
477 // which will destroy the session, but we can't rely on the client for that.
478 sockVal.emit("timeout");
481 // Disconnect socket.
482 sockVal.disconnect();
487 sessionID = key.replace(sessionStore.prefix, '');
489 // Destroy session here incase the client never hits /logout.
490 sessionStore.destroy(sessionID, function (err) {
491 //X.debug("Session destroied: ", key, " error: ", err);
495 // TODO - Use NODE_ENV flag to switch between development and production.
496 // See "Understanding the configure method" at:
497 // https://github.com/LearnBoost/Socket.IO/wiki/Configuring-Socket.IO
498 io.configure(function () {
501 io.set('log', false);
502 // TODO - We need to implement a store for this if we run multiple processes:
503 // https://github.com/LearnBoost/socket.io/tree/0.9/lib/stores
504 //http://stackoverflow.com/questions/9267292/examples-in-using-redisstore-in-socket-io/9275798#9275798
505 //io.set('store', someNewStore); // Use our someNewStore.
506 io.set('browser client minification', true); // Send minified file to the client.
507 io.set('browser client etag', true); // Apply etag caching logic based on version number
508 // TODO - grubmle - See prototype stomp above:
509 // https://github.com/LearnBoost/socket.io/issues/932
510 // https://github.com/LearnBoost/socket.io/issues/984
511 io.set('browser client gzip', true); // gzip the file.
512 //io.set('log level', 1); // Reduce logging.
513 io.set('transports', [ // Enable all transports.
523 * Setup socket.io routes and handlers.
525 * Socket.io authorization modeled off of:
526 * https://github.com/LearnBoost/socket.io/wiki/Authorizing
527 * http://stackoverflow.com/questions/13095418/how-to-use-passport-with-express-and-socket-io
528 * https://github.com/jfromaniello/passport.socketio
530 io.of('/clientsock').authorization(function (handshakeData, callback) {
535 if (handshakeData.headers.cookie) {
536 handshakeData.cookie = cookie.parse(handshakeData.headers.cookie);
538 if (handshakeData.headers.referer && url.parse(handshakeData.headers.referer).path.split("/")[1]) {
539 key = url.parse(handshakeData.headers.referer).path.split("/")[1];
540 } else if (X.options.datasource.testDatabase) {
541 // for some reason zombie doesn't send the referrer in the socketio call
542 // https://groups.google.com/forum/#!msg/socket_io/MPpXrP5N9k8/xAyk1l8Iw8YJ
543 key = X.options.datasource.testDatabase;
545 return callback(null, false);
549 if (!handshakeData.cookie[key + '.sid']) {
550 return callback(null, false);
553 // Add sessionID so we can use it to check for valid sessions on each request below.
554 handshakeData.sessionID = parseSignedCookie(handshakeData.cookie[key + '.sid'], privateSalt);
556 sessionStore.get(handshakeData.sessionID, function (err, session) {
558 return callback(err);
561 // All requests get a session. Make sure the session is authenticated.
562 if (!session || !session.passport || !session.passport.user ||
563 !session.passport.user.id ||
564 !session.passport.user.organization ||
565 !session.passport.user.username ||
566 !session.cookie || !session.cookie.expires) {
568 destroySession(handshakeData.sessionID, session);
570 // Not an error exactly, but the cookie is invalid. The user probably logged off.
571 return callback(null, false);
574 // Prep the cookie and create a session object so we can touch() it on each request below.
575 session.cookie.expires = new Date(session.cookie.expires);
576 session.cookie = new Cookie(session.cookie);
577 handshakeData.session = new Session(handshakeData, session);
579 // Add sessionStore here so it can be used to lookup valid session on each request below.
580 handshakeData.sessionStore = sessionStore;
583 callback(null, true);
586 callback(null, false);
588 }).on('connection', function (socket) {
591 var ensureLoggedIn = function (callback, payload) {
592 socket.handshake.sessionStore.get(socket.handshake.sessionID, function (err, session) {
596 // All requests get a session. Make sure the session is authenticated.
597 if (err || !session || !session.passport || !session.passport.user ||
598 !session.passport.user.id ||
599 !session.passport.user.organization ||
600 !session.passport.user.username ||
601 !session.cookie || !session.cookie.expires) {
603 return destroySession(socket.handshake.sessionID, session);
606 // Make sure the sesion hasn't expired yet.
607 expires = new Date(session.cookie.expires);
608 current = new Date();
609 if (expires <= current) {
610 return destroySession(socket.handshake.sessionID, session);
612 // User is still valid
614 // Update session expiration timeout, unless this is an automated call of
615 // some sort (e.g. lock refresh)
616 if (!payload || !payload.automatedRefresh) {
617 socket.handshake.session.touch().save();
626 // Save socket.id to the session store so we can disconnect that socket server side
627 // when a session is timed out. That should notify the client imediately they have timed out.
628 socket.handshake.session.socket = {id: socket.id};
629 socket.handshake.session.save();
631 // To run this from the client:
633 socket.on('session', function (data, callback) {
634 ensureLoggedIn(function (session) {
635 var callbackObj = X.options.client || {};
636 callbackObj = _.extend(callbackObj,
638 data: session.passport.user,
640 debugging: X.options.datasource.debugging,
641 biAvailable: _.isObject(X.options.biServer) && !_.isEmpty(X.options.biServer),
642 emailAvailable: _.isString(X.options.datasource.smtpHost) && X.options.datasource.smtpHost !== "",
643 printAvailable: _.isString(X.options.datasource.printer) && X.options.datasource.printer !== "",
646 callback(callbackObj);
647 }, data && data.payload);
650 // To run this from the client:
651 socket.on('delete', function (data, callback) {
652 ensureLoggedIn(function (session) {
653 routes.queryDatabase("delete", data.payload, session, callback);
654 }, data && data.payload);
657 // To run this from the client:
658 // XT.dataSource.request(m = new XM.Contact(), "get", {nameSpace: "XM", type: "Contact", id: "1"}, {propagate: true, parse: true, success: function () {console.log("success", arguments)}, error: function () {console.log("error", arguments);}});
659 socket.on('get', function (data, callback) {
660 ensureLoggedIn(function (session) {
661 routes.queryDatabase("get", data.payload, session, callback);
662 }, data && data.payload);
665 // To run this from the client:
666 socket.on('patch', function (data, callback) {
667 ensureLoggedIn(function (session) {
668 routes.queryDatabase("patch", data.payload, session, callback);
669 }, data && data.payload);
672 // To run this from the client:
673 socket.on('post', function (data, callback) {
674 ensureLoggedIn(function (session) {
675 routes.queryDatabase("post", data.payload, session, callback);
676 }, data && data.payload);
679 // Tell the client it's connected.
684 * Job loading section.
686 * The following are jobs that must be started at start up or scheduled to run periodically.
689 // TODO - Check pid file to see if this is already running.
690 // Kill process or create new pid file.
692 // Run the expireSessions cleanup/garbage collection once a minute.
693 setInterval(function () {
696 //X.debug("session cleanup called at: ", new Date());
697 sessionStore.expireSessions(destroySession);