jsbin/lib/app.js
2018-04-09 21:48:06 +01:00

351 lines
11 KiB
JavaScript

const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const expressSession = require('express-session');
const cookieSession = require('cookie-session');
const morgan = require('morgan');
const favicon = require('serve-favicon');
var nodemailer = require('nodemailer'),
express = require('express'),
flatten = require('flatten.js').flatten,
path = require('path'),
app = express(),
hbs = require('./hbs'),
options = require('./config'),
openshift = require('./addons/openshift')(options), // injects options from openshift env vars
store = require('./store')(options.store),
undefsafe = require('undefsafe'),
models = require('./models').createModels(store),
routes = require('./routes'),
handlers = require('./handlers'),
middleware = require('./middleware'),
helpers = require('./helpers'),
metrics = require('./metrics'),
github = require('./github')(options), // if used, contains github.id
_ = require('lodash'),
crypto = require('crypto'),
filteredCookieSession = require('./blacklist-cookie'),
sessionVersion = require('./session-version'),
stripeRoutes = require('./stripe')(options),
flattened;
function generateSessionSecret() {
console.log('Warning: Generating a session key - please see http://jsbin.com/help/session-secret');
return crypto.createHash('md5').update(Math.random() + '').digest('hex');
}
/**
* JS Bin configuration
*/
app.store = store;
app.sessionVersion = sessionVersion;
// Create model singletons
// models.createModels(store);
app.mailer = (function (mail) {
var mailTransport = null,
method = mail && mail.adapter,
settings = mail && mail[method];
if (method && options) {
mailTransport = nodemailer.createTransport(method, settings);
}
return new handlers.MailHandler(mailTransport, app.render.bind(app));
})(options.mail);
app.PRODUCTION = 'production';
app.DEVELOPMENT = 'development';
// Set the NODE_ENV variable as this is used by Express, we want the
// environment to take precedence but allow it to be set using the config file
// too.
if (process.env.NODE_ENV) {
options.env = process.env.NODE_ENV;
}
process.env.NODE_ENV = options.env;
// Need to set the node environment to run in the same timezone as the database
// This will ideally be UTC in both cases but if not this can be set either
// using the TZ environment variable or the "timezone" option.
// This is because the mysql library coerces MySQL dates (without timezones)
// into JavaScript date objects.
if (!process.env.TZ && options.timezone) {
process.env.TZ = options.timezone;
}
// Sort out the port.
(function () {
var port = process.env.PORT;
// if we're running from the bin/jsbin file, then we modify
// both the listen port AND the options.url
if (process.env.JSBIN_PORT) {
if (options.url.host.indexOf(':') === -1) {
options.url.host += ':' + process.env.JSBIN_PORT;
} else {
options.url.host = options.url.host.replace(/\:\d+$/, function () {
return ':' + process.env.JSBIN_PORT;
});
}
}
if (!port) {
options.url.host.replace(/\:(\d+)$/, function (m, p) {
if (p.length) {
port = p;
}
});
}
if (!port) {
port = 80;
}
// NOTE: this is *only* used for running the server.listen()
// it's not used in the urls to access assets, etc.
options.port = port;
})();
// Strip trailing slash from the prefix
options.url.prefix = options.url.prefix.replace(/\/$/, '');
// Apply the keys from the config file. All nested properties are
// space delimited to match the express style.
//
// For example, app.set('url prefix'); //=> '/'
flattened = flatten(options, ' ');
Object.getOwnPropertyNames(flattened).forEach(function (key) {
app.set(key, flattened[key]);
});
// in live, we run behind a proxy - so this will give us our IPs again:
// http://expressjs.com/guide.html#proxies
if (process.env.NODE_ENV === app.PRODUCTION || process.env.JSBIN_PROXY) {
app.set('trust proxy', true);
}
app.disable('x-powered-by');
app.set('root', path.resolve(path.join(__dirname, '..')));
// ensure jsbin is running from the expected root
process.chdir(app.get('root'));
app.set('version', require('../package').version);
app.set('url prefix', options.url.prefix);
app.set('url ssl', options.url.ssl);
app.set('url full', 'http://' + app.get('url host') + app.get('url prefix'));
app.set('basepath', app.get('url prefix'));
app.set('is_production', process.env.NODE_ENV === app.PRODUCTION);
if (options.url.static) {
app.set('static url', 'http://' + app.get('url static'));
} else {
app.set('static url', app.get('url full'));
}
if (options.url.runner) {
// strip trailing slash, just in case
options.url.runner = options.url.runner.replace(/\/$/, '');
app.set('url runner', options.url.runner);
} else {
app.set('url runner', app.get('url full'));
}
app.set('views', 'views');
app.set('view engine', 'html');
app.engine('html', hbs.express3({
extname: '.html',
partialsDir: [__dirname + '/../views/partials'],
}));
// app.engine('txt', hbs.express3({
// extname: '.txt',
// partialsDir: [
// path.resolve(__dirname + '/../views/partials')
// ]
// }));
// Define some global template variables.
var helpers = helpers.createHelpers(app);
app.locals.version = app.get('version');
app.locals.client = app.get('client');
Object.keys(helpers).forEach(function (key) {
app.locals[key] = helpers[key];
});
// set these as global template variables since they keep getting missed
app.locals.is_production = app.get('is_production');
app.locals.cachebust = app.locals.cacheBust = app.get('is_production') ? '?' + app.get('version') : '';
if (!app.get('session secret')) {
app.set('session secret', generateSessionSecret());
}
app.connect = function (callback) {
app.emit('before:connect', app);
// Register all the middleware.
var mount = app.set('url prefix') || '/',
logger;
logger = process.env.JSBIN_LOGGER || app.get('server logger') || 'tiny';
if (logger !== 'none') {
app.use(morgan(logger));
}
// favicon
app.use(favicon(path.join(__dirname, '../', 'public','images','favicon.png')));
app.use(function (req, res, next) {
// used for timings
req.start = Date.now();
next();
});
app.emit('before:middleware');
app.use(middleware.flash());
// this middleware says:
// a) if we have a static url in our config, i.e. static.jsbin.com
// b) only serve static assets IF AND ONLY IF, the full url is requested,
// i.e. static.jsbin.com/css/style.css
// c) otherwise, skip the static handler, and go straight to routes.js
//
// If the request hits routes.js and doesn't match a defined route (or doesn't
// look like a bin url) then it gets 404'd - i.e. you can't request /css/style.css
// because it'll return a 404.
app.use(mount, (function () {
var staticRouter = express.static(path.join(app.get('root'), 'public'),
{maxAge: (options['max-age'] || 86400) * 1000});
return function (req, res, next) {
// construct the full request url
var url = req.protocol + '://' + req.get('host') + req.url;
// *if* the config has a static url path (i.e. different from jsbin.com),
// i.e. static.jsbin.com
if (options.url.static) {
// and the url requested is NOT static, then continue with node
if (url.indexOf(options.url.static) === -1) {
return next();
}
}
// otherwise serve it as a static asset...
return staticRouter(req, res, next);
};
})());
app.use(middleware.limitContentLength({limit: app.get('max-request-size')}));
app.emit('before:cookies', { app: app });
app.use(cookieParser(app.get('session secret')));
app.use(cookieSession({
keys: ['jsbin'],
secure: false,
maxAge: 365 * 24 * 60 * 60 * 1000,
// the domain must contain a dot and should not have a port
domain: app.get('url host').indexOf('.') === -1 ? undefined : '.' + app.get('url host').replace(/:\d+$/, ''),
}));
app.emit('before:session', {app: app});
// memcached sessions
if (options.session.memcached && options.session.memcached.connection) {
require('./addons/memcached')(app, options.session.memcached.connection);
}
if (options.url.host !== 'jsbin.com') {
require('./ping');
}
// this needs to be last, because it hooks an onHeaders, which
// "fires in reverse order" so it needs to go first.
app.use(filteredCookieSession(['user.settings', 'passport', 'user.email', 'user.github_id', 'user.github_token', 'user.dropbox_token', 'user.dropbox_id', 'user.flagged', 'user.bincount', 'user.api_key', 'user.key', 'user.id', 'user.embed']));
app.emit('after:session', {app: app});
app.use(function (req, res, next) {
// Deletes cookies using the old domain (without the leading '.')
// and we know this because they don't have a version in their cookie.
if (undefsafe(req, 'session.version') === undefined) {
var oldSess = _.extend({ version: app.get('version') }, req.session);
res.clearCookie('jsbin');
req.session = oldSess;
}
next();
});
// Passport
if (options.github && options.github.id) {
github.initialize(app);
}
if (options.dropbox && options.dropbox.id) {
require('./dropbox')(options).initialize();
}
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(middleware.csrf({ ignore: ['/', /^\/api\//, /^\/hooks\//, /^\/subscribe\//] }));
app.use(middleware.api({ app: app }));
app.use(middleware.subdomain(app));
app.use(middleware.noslashes());
app.use(middleware.protectServiceWorker({ allowed: ['/sw.js', '/sw-runner.js' ]}));
app.use(middleware.ajax());
// app.use(middleware.cors());
app.use(middleware.jsonp());
app.use(middleware.protocolCheck);
app.use(middleware.decompressBody);
app.use(middleware.notices());
app.use(middleware.nofollow({
allowed: ['/']
}));
app.emit('before:router', app);
// All routes must be registered after middleware
// Register stripe webhooks
if (options.payment && options.payment.stripe && options.store.adapter === 'mysql') {
stripeRoutes(app);
}
// Register all routes
app.use(mount, routes(app));
app.emit('after', app);
store.connect(function (err) {
if (err) {
metrics.increment('error.store.connect');
throw err;
}
var port = app.set('port');
app.listen(port, options.host); // options.host can be empty, meaning "bind to all network interfaces"
if (typeof callback === 'function') {
callback();
}
app.emit('connected');
process.stdout.write('JS Bin v' + app.get('version') + ' is up and running on port ' + app.get('port') + '. Now point your browser at ' + app.get('url full') + '\n');
});
};
// Export the application to allow it to be included.
module.exports = { app: app };
// Run a local development server if this file is called directly.
if (require.main === module) {
app.connect();
}