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 = X.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 loadExtensionServerside = function (extension) {
118 var packagePath = X.path.join(getExtensionDir(extension), "package.json");
119 var packageJson = X.fs.existsSync(packagePath) ? require(packagePath) : undefined;
120 var manifestPath = X.path.join(getExtensionDir(extension), "database/source/manifest.js");
121 var manifest = X.fs.existsSync(manifestPath) ? JSON.parse(X.fs.readFileSync(manifestPath)) : {};
122 var version = packageJson ? packageJson.version : manifest.version;
123 X.versions[extension.name] = version || "none"; // XXX the "none" is temporary until we have core extensions in npm
125 // TODO: be able to define routes in package.json
126 _.each(manifest.routes || [], function (routeDetails) {
127 var verb = (routeDetails.verb || "all").toLowerCase(),
128 func = require(X.path.join(getExtensionDir(extension),
129 "node-datasource", routeDetails.filename))[routeDetails.functionName];
131 if (_.contains(["all", "get", "post", "patch", "delete"], verb)) {
132 app[verb]('/:org/' + routeDetails.path, func);
134 console.log("Invalid verb for extension-defined route " + routeDetails.path);
139 schemaSessionOptions.username = X.options.databaseServer.user;
140 schemaSessionOptions.database = X.options.datasource.databases[0];
141 // XXX note that I'm not addressing an underlying bug that we don't wait to
142 // listen on the port until all the setup is done
143 schemaSessionOptions.success = function () {
147 var extensions = new SYS.ExtensionCollection();
149 database: X.options.datasource.databases[0],
150 success: function (coll, results, options) {
152 // XXX time bomb: assuming app has been initialized, below, by now
153 XT.log("Could not load extension routes or client-side code because the app has not started");
157 useClientDir("/client", "../enyo-client/application");
158 _.each(results, loadExtensionServerside);
159 _.each(results, loadExtensionClientside);
163 XT.session.loadSessionObjects(XT.session.SCHEMA, schemaSessionOptions);
165 privSessionOptions.username = X.options.databaseServer.user;
166 privSessionOptions.database = X.options.datasource.databases[0];
167 XT.session.loadSessionObjects(XT.session.PRIVILEGES, privSessionOptions);
173 Grab the version number from the package.json file.
176 var packageJson = X.fs.readFileSync("../package.json");
178 core: JSON.parse(packageJson).version
182 * Module dependencies.
184 var passport = require('passport'),
185 oauth2 = require('./oauth2/oauth2'),
186 routes = require('./routes/routes'),
187 socketio = require('socket.io'),
188 url = require('url'),
189 utils = require('./oauth2/utils'),
190 user = require('./oauth2/user'),
193 // TODO - for testing. remove...
194 //http://stackoverflow.com/questions/13091037/node-js-heap-snapshots-and-google-chrome-snapshot-viewer
195 //var heapdump = require("heapdump");
196 // Use it!: https://github.com/c4milo/node-webkit-agent
197 //var agent = require('webkit-devtools-agent');
200 * ###################################################
203 * Sometimes we need to change how an npm packages works.
204 * Don't edit the packages directly, override them here.
205 * ###################################################
209 Define our own authentication criteria for passport. Passport itself defines
210 its authentication function here:
211 https://github.com/jaredhanson/passport/blob/master/lib/passport/http/request.js#L74
212 We are stomping on that method with our own special business logic.
213 The ensureLoggedIn function will not need to be changed, because that calls this.
215 require('http').IncomingMessage.prototype.isAuthenticated = function () {
218 var creds = this.session.passport.user;
220 if (creds && creds.id && creds.username && creds.organization) {
223 destroySession(this.sessionID, this.session);
228 // Stomping on express/connect's Cookie.prototype to only update the expires property
229 // once a minute. Otherwise it's hit on every session check. This cuts down on chatter.
230 // See more details here: https://github.com/senchalabs/connect/issues/670
231 require('express/node_modules/connect/lib/middleware/session/cookie').prototype.__defineSetter__("expires", require('./stomps/expires').expires);
233 // Stomp on Express's cookie serialize() to not send an "expires" value to the browser.
234 // This makes the browser cooke a "session" cookie that will never expire and only
235 // gets removed when the user closes the browser. We still set express.session.cookie.maxAge
236 // below so our persisted session gets an expires value, but not the browser cookie.
237 // See this issue for more details: https://github.com/senchalabs/connect/issues/328
238 require('express/node_modules/cookie').serialize = require('./stomps/cookie').serialize;
240 // Stomp on Connect's session.
241 // https://github.com/senchalabs/connect/issues/641
242 function stompSessionLoad() {
244 return require('./stomps/session');
246 require('express/node_modules/connect').middleware.__defineGetter__('session', stompSessionLoad);
247 require('express/node_modules/connect').__defineGetter__('session', stompSessionLoad);
248 require('express').__defineGetter__('session', stompSessionLoad);
251 * ###################################################
252 * END Overrides section.
253 * ###################################################
261 sslOptions.key = X.fs.readFileSync(X.options.datasource.keyFile);
262 if (X.options.datasource.caFile) {
263 sslOptions.ca = _.map(X.options.datasource.caFile, function (obj) {
266 return X.fs.readFileSync(obj);
269 sslOptions.cert = X.fs.readFileSync(X.options.datasource.certFile);
272 * Express configuration.
276 var server = X.https.createServer(sslOptions, app),
277 parseSignedCookie = require('express/node_modules/connect').utils.parseSignedCookie,
278 //MemoryStore = express.session.MemoryStore,
279 XTPGStore = require('./oauth2/db/connect-xt-pg')(express),
281 //sessionStore = new MemoryStore(),
282 sessionStore = new XTPGStore({ hybridCache: X.options.datasource.requireCache || false }),
283 Session = require('express/node_modules/connect/lib/middleware/session').Session,
284 Cookie = require('express/node_modules/connect/lib/middleware/session/cookie'),
285 cookie = require('express/node_modules/cookie'),
286 privateSalt = X.fs.readFileSync(X.options.datasource.saltFile).toString() || 'somesecret';
288 // Conditionally load express.session(). REST API endpoints using OAuth tokens do not get sessions.
289 var conditionalExpressSession = function (req, res, next) {
294 // REST API endpoints start with "/api" in their path.
295 // The 'assets' folder and login page are sessionless.
296 if ((/^api/i).test(req.path.split("/")[2]) ||
297 (/^\/assets/i).test(req.path) ||
299 req.path === "/favicon.ico" ||
300 req.path === "/forgot-password" ||
301 req.path === '/node_modules/jquery/jquery.js' ||
302 req.path === "/recover") {
306 if (req.path === "/login") {
307 // TODO - Add check against X.options database array
308 key = req.body.database + ".sid";
309 } else if (req.path.split("/")[1]) {
310 key = req.path.split("/")[1] + ".sid";
312 // TODO - Dynamically name the cookie after the database.
313 console.log("### FIX ME ### setting cookie name to 'connect.sid' for path = ", JSON.stringify(req.path));
314 console.log("### FIX ME ### cookie name should match database name!!!");
315 console.trace("### At this location ###");
319 // Instead of doing app.use(express.session()) we call the package directly
320 // which returns a function (req, res, next) we can call to do the same thing.
321 var init_session = express.session({
325 // See cookie stomp above for more details on how this session cookie works.
330 maxAge: (X.options.datasource.sessionTimeout * 60 * 1000) || 3600000
332 sessionIDgen: function () {
333 // TODO: Stomp on connect's sessionID generate.
334 // https://github.com/senchalabs/connect/issues/641
335 return key.split(".")[0] + "." + utils.generateUUID();
339 init_session(req, res, next);
343 // Conditionally load passport.session(). REST API endpoints using OAuth tokens do not get sessions.
344 var conditionalPassportSession = function (req, res, next) {
347 // REST API endpoints start with "/api" in their path.
348 // The 'assets' folder and login page are sessionless.
349 if ((/^api/i).test(req.path.split("/")[2]) ||
350 (/^\/assets/i).test(req.path) ||
352 req.path === "/favicon.ico"
357 // Instead of doing app.use(passport.session())
358 var init_passportSessions = passport.session();
360 init_passportSessions(req, res, next);
364 app.configure(function () {
367 // gzip all static files served.
368 app.use(express.compress());
369 // Add a basic view engine that will render files from "views" directory.
370 app.set('view engine', 'ejs');
372 // TODO - This outputs access logs like apache2 and some other user things.
373 //app.use(express.logger());
375 app.use(express.cookieParser());
376 app.use(express.bodyParser());
378 // Conditionally load session packages. Based off these examples:
379 // http://stackoverflow.com/questions/9348505/avoiding-image-logging-in-express-js/9351428#9351428
380 // http://stackoverflow.com/questions/13516898/disable-csrf-validation-for-some-requests-on-express
381 app.use(conditionalExpressSession);
382 app.use(passport.initialize());
383 app.use(conditionalPassportSession);
386 app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
390 * Passport configuration.
392 require('./oauth2/passport');
395 * Setup HTTP routes and handlers.
399 app.use(express.favicon(__dirname + '/views/login/assets/favicon.ico'));
400 app.use('/assets', express.static('views/login/assets', { maxAge: 86400000 }));
401 app.use('/node_modules/jquery', express.static('../node_modules/jquery/dist', { maxAge: 86400000 }));
403 app.get('/:org/dialog/authorize', oauth2.authorization);
404 app.post('/:org/dialog/authorize/decision', oauth2.decision);
405 app.post('/:org/oauth/token', oauth2.token);
407 app.get('/:org/discovery/v1alpha1/apis/v1alpha1/rest', routes.restDiscoveryGetRest);
408 app.get('/:org/discovery/v1alpha1/apis/:model/v1alpha1/rest', routes.restDiscoveryGetRest);
409 app.get('/:org/discovery/v1alpha1/apis', routes.restDiscoveryList);
411 app.get('/:org/api/userinfo', user.info);
413 app.post('/:org/api/v1alpha1/services/:service/:id', routes.restRouter);
414 app.all('/:org/api/v1alpha1/resources/:model/:id', routes.restRouter);
415 app.all('/:org/api/v1alpha1/resources/:model', routes.restRouter);
416 app.all('/:org/api/v1alpha1/resources/*', routes.restRouter);
418 app.get('/', routes.loginForm);
419 app.post('/login', routes.login);
420 app.get('/forgot-password', routes.forgotPassword);
421 app.post('/recover', routes.recoverPassword);
422 app.get('/:org/recover/reset/:id/:token', routes.verifyRecoverPassword);
423 app.post('/:org/recover/resetUpdate', routes.resetRecoveredPassword);
424 app.get('/login/scope', routes.scopeForm);
425 app.post('/login/scopeSubmit', routes.scope);
426 app.get('/logout', routes.logout);
427 app.get('/:org/logout', routes.logout);
428 app.get('/:org/app', routes.app);
429 app.get('/:org/debug', routes.debug);
431 app.all('/:org/credit-card', routes.creditCard);
432 app.all('/:org/change-password', routes.changePassword);
433 app.all('/:org/client/build/client-code', routes.clientCode);
434 app.all('/:org/email', routes.email);
435 app.all('/:org/export', routes.exxport);
436 app.get('/:org/file', routes.file);
437 app.get('/:org/generate-report', routes.generateReport);
438 app.all('/:org/install-extension', routes.installExtension);
439 app.get('/:org/locale', routes.locale);
440 app.all('/:org/oauth/generate-key', routes.generateOauthKey);
441 app.get('/:org/reset-password', routes.resetPassword);
442 app.post('/:org/oauth/revoke-token', routes.revokeOauthToken);
443 app.all('/:org/vcfExport', routes.vcfExport);
446 // Set up the other servers we run on different ports.
448 var redirectServer = express();
449 redirectServer.get(/.*/, routes.redirect); // RegEx for "everything"
450 redirectServer.listen(X.options.datasource.redirectPort, X.options.datasource.bindAddress);
453 * Start the express server. This is the NEW way.
455 // TODO - Active browser sessions can make calls to this server when it hasn't fully started.
456 // That can cause it to crash at startup.
457 // Need a way to get everything loaded BEFORE we start listening. Might just move this to the end...
458 io = socketio.listen(server.listen(X.options.datasource.port, X.options.datasource.bindAddress));
460 X.log("Server listening at: ", X.options.datasource.bindAddress);
461 X.log("node-datasource started on port: ", X.options.datasource.port);
462 X.log("redirectServer started on port: ", X.options.datasource.redirectPort);
463 X.log("Databases accessible from this server: \n", JSON.stringify(X.options.datasource.databases, null, 2));
467 * Destroy a single session.
468 * @param {Object} val - Session object.
469 * @param {String} key - Session id.
471 destroySession = function (key, val) {
477 if (val && val.socket && val.socket.id) {
478 _.each(io.sockets.sockets, function (sockVal, sockKey, sockList) {
479 if (val.socket.id === sockKey) {
480 _.each(sockVal.manager.namespaces, function (spaceVal, spaceKey, spaceList) {
481 sockVal.flags.endpoint = spaceVal.name;
482 // Tell the client it timed out. This will redirect the client to /logout
483 // which will destroy the session, but we can't rely on the client for that.
484 sockVal.emit("timeout");
487 // Disconnect socket.
488 sockVal.disconnect();
493 sessionID = key.replace(sessionStore.prefix, '');
495 // Destroy session here incase the client never hits /logout.
496 sessionStore.destroy(sessionID, function (err) {
497 //X.debug("Session destroied: ", key, " error: ", err);
501 // TODO - Use NODE_ENV flag to switch between development and production.
502 // See "Understanding the configure method" at:
503 // https://github.com/LearnBoost/Socket.IO/wiki/Configuring-Socket.IO
504 io.configure(function () {
507 io.set('log', false);
508 // TODO - We need to implement a store for this if we run multiple processes:
509 // https://github.com/LearnBoost/socket.io/tree/0.9/lib/stores
510 //http://stackoverflow.com/questions/9267292/examples-in-using-redisstore-in-socket-io/9275798#9275798
511 //io.set('store', someNewStore); // Use our someNewStore.
512 io.set('browser client minification', true); // Send minified file to the client.
513 io.set('browser client etag', true); // Apply etag caching logic based on version number
514 // TODO - grubmle - See prototype stomp above:
515 // https://github.com/LearnBoost/socket.io/issues/932
516 // https://github.com/LearnBoost/socket.io/issues/984
517 io.set('browser client gzip', true); // gzip the file.
518 //io.set('log level', 1); // Reduce logging.
519 io.set('transports', [ // Enable all transports.
529 * Setup socket.io routes and handlers.
531 * Socket.io authorization modeled off of:
532 * https://github.com/LearnBoost/socket.io/wiki/Authorizing
533 * http://stackoverflow.com/questions/13095418/how-to-use-passport-with-express-and-socket-io
534 * https://github.com/jfromaniello/passport.socketio
536 io.of('/clientsock').authorization(function (handshakeData, callback) {
541 if (handshakeData.headers.cookie) {
542 handshakeData.cookie = cookie.parse(handshakeData.headers.cookie);
544 if (handshakeData.headers.referer && url.parse(handshakeData.headers.referer).path.split("/")[1]) {
545 key = url.parse(handshakeData.headers.referer).path.split("/")[1];
546 } else if (X.options.datasource.testDatabase) {
547 // for some reason zombie doesn't send the referrer in the socketio call
548 // https://groups.google.com/forum/#!msg/socket_io/MPpXrP5N9k8/xAyk1l8Iw8YJ
549 key = X.options.datasource.testDatabase;
551 return callback(null, false);
555 if (!handshakeData.cookie[key + '.sid']) {
556 return callback(null, false);
559 // Add sessionID so we can use it to check for valid sessions on each request below.
560 handshakeData.sessionID = parseSignedCookie(handshakeData.cookie[key + '.sid'], privateSalt);
562 sessionStore.get(handshakeData.sessionID, function (err, session) {
564 return callback(err);
567 // All requests get a session. Make sure the session is authenticated.
568 if (!session || !session.passport || !session.passport.user ||
569 !session.passport.user.id ||
570 !session.passport.user.organization ||
571 !session.passport.user.username ||
572 !session.cookie || !session.cookie.expires) {
574 destroySession(handshakeData.sessionID, session);
576 // Not an error exactly, but the cookie is invalid. The user probably logged off.
577 return callback(null, false);
580 // Prep the cookie and create a session object so we can touch() it on each request below.
581 session.cookie.expires = new Date(session.cookie.expires);
582 session.cookie = new Cookie(session.cookie);
583 handshakeData.session = new Session(handshakeData, session);
585 // Add sessionStore here so it can be used to lookup valid session on each request below.
586 handshakeData.sessionStore = sessionStore;
589 callback(null, true);
592 callback(null, false);
594 }).on('connection', function (socket) {
597 var ensureLoggedIn = function (callback, payload) {
598 socket.handshake.sessionStore.get(socket.handshake.sessionID, function (err, session) {
602 // All requests get a session. Make sure the session is authenticated.
603 if (err || !session || !session.passport || !session.passport.user ||
604 !session.passport.user.id ||
605 !session.passport.user.organization ||
606 !session.passport.user.username ||
607 !session.cookie || !session.cookie.expires) {
609 return destroySession(socket.handshake.sessionID, session);
612 // Make sure the sesion hasn't expired yet.
613 expires = new Date(session.cookie.expires);
614 current = new Date();
615 if (expires <= current) {
616 return destroySession(socket.handshake.sessionID, session);
618 // User is still valid
620 // Update session expiration timeout, unless this is an automated call of
621 // some sort (e.g. lock refresh)
622 if (!payload || !payload.automatedRefresh) {
623 socket.handshake.session.touch().save();
632 // Save socket.id to the session store so we can disconnect that socket server side
633 // when a session is timed out. That should notify the client imediately they have timed out.
634 socket.handshake.session.socket = {id: socket.id};
635 socket.handshake.session.save();
637 // To run this from the client:
639 socket.on('session', function (data, callback) {
640 ensureLoggedIn(function (session) {
641 var callbackObj = X.options.client || {};
642 callbackObj = _.extend(callbackObj,
644 data: session.passport.user,
646 debugging: X.options.datasource.debugging,
647 emailAvailable: _.isString(X.options.datasource.smtpHost) && X.options.datasource.smtpHost !== "",
648 printAvailable: _.isString(X.options.datasource.printer) && X.options.datasource.printer !== "",
651 callback(callbackObj);
652 }, data && data.payload);
655 // To run this from the client:
656 socket.on('delete', function (data, callback) {
657 ensureLoggedIn(function (session) {
658 routes.queryDatabase("delete", data.payload, session, callback);
659 }, data && data.payload);
662 // To run this from the client:
663 // 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);}});
664 socket.on('get', function (data, callback) {
665 ensureLoggedIn(function (session) {
666 routes.queryDatabase("get", data.payload, session, callback);
667 }, data && data.payload);
670 // To run this from the client:
671 socket.on('patch', function (data, callback) {
672 ensureLoggedIn(function (session) {
673 routes.queryDatabase("patch", data.payload, session, callback);
674 }, data && data.payload);
677 // To run this from the client:
678 socket.on('post', function (data, callback) {
679 ensureLoggedIn(function (session) {
680 routes.queryDatabase("post", data.payload, session, callback);
681 }, data && data.payload);
684 // Tell the client it's connected.
689 * Job loading section.
691 * The following are jobs that must be started at start up or scheduled to run periodically.
694 // TODO - Check pid file to see if this is already running.
695 // Kill process or create new pid file.
697 // Run the expireSessions cleanup/garbage collection once a minute.
698 setInterval(function () {
701 //X.debug("session cleanup called at: ", new Date());
702 sessionStore.expireSessions(destroySession);