// app // external modules var express = require('express') var toobusy = require('toobusy-js') var ejs = require('ejs') var passport = require('passport') var methodOverride = require('method-override') var cookieParser = require('cookie-parser') var bodyParser = require('body-parser') var compression = require('compression') var session = require('express-session') var SequelizeStore = require('connect-session-sequelize')(session.Store) var fs = require('fs') var url = require('url') var path = require('path') var imgur = require('imgur') var formidable = require('formidable') var morgan = require('morgan') var passportSocketIo = require('passport.socketio') var helmet = require('helmet') var i18n = require('i18n') var flash = require('connect-flash') var validator = require('validator') // core var config = require('./lib/config.js') var logger = require('./lib/logger.js') var auth = require('./lib/auth.js') var response = require('./lib/response.js') var models = require('./lib/models') // server setup var app = express() var server = null if (config.usessl) { var ca = (function () { var i, len, results results = [] for (i = 0, len = config.sslcapath.length; i < len; i++) { results.push(fs.readFileSync(config.sslcapath[i], 'utf8')) } return results })() var options = { key: fs.readFileSync(config.sslkeypath, 'utf8'), cert: fs.readFileSync(config.sslcertpath, 'utf8'), ca: ca, dhparam: fs.readFileSync(config.dhparampath, 'utf8'), requestCert: false, rejectUnauthorized: false } server = require('https').createServer(options, app) } else { server = require('http').createServer(app) } // logger app.use(morgan('combined', { 'stream': logger.stream })) // socket io var io = require('socket.io')(server) io.engine.ws = new (require('uws').Server)({ noServer: true, perMessageDeflate: false }) // others var realtime = require('./lib/realtime.js') // assign socket io to realtime realtime.io = io // methodOverride app.use(methodOverride('_method')) // create application/x-www-form-urlencoded parser var urlencodedParser = bodyParser.urlencoded({ extended: false, limit: 1024 * 1024 * 10 // 10 mb }) // session store var sessionStore = new SequelizeStore({ db: models.sequelize }) // compression app.use(compression()) // use hsts to tell https users stick to this app.use(helmet.hsts({ maxAge: 31536000 * 1000, // 365 days includeSubdomains: true, preload: true })) i18n.configure({ locales: ['en', 'zh', 'fr', 'de', 'ja', 'es', 'el', 'pt', 'it', 'tr', 'ru', 'nl', 'hr', 'pl', 'uk', 'hi', 'sv', 'eo'], cookie: 'locale', directory: path.join(__dirname, '/locales') }) app.use(cookieParser()) app.use(i18n.init) // routes without sessions // static files app.use('/', express.static(path.join(__dirname, '/public'), { maxAge: config.staticcachetime })) // session app.use(session({ name: config.sessionname, secret: config.sessionsecret, resave: false, // don't save session if unmodified saveUninitialized: true, // always create session to ensure the origin rolling: true, // reset maxAge on every response cookie: { maxAge: config.sessionlife }, store: sessionStore })) // session resumption var tlsSessionStore = {} server.on('newSession', function (id, data, cb) { tlsSessionStore[id.toString('hex')] = data cb() }) server.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null) }) // middleware which blocks requests when we're too busy app.use(function (req, res, next) { if (toobusy()) { response.errorServiceUnavailable(res) } else { next() } }) app.use(flash()) // passport app.use(passport.initialize()) app.use(passport.session()) auth.registerAuthMethod() // serialize and deserialize passport.serializeUser(function (user, done) { logger.info('serializeUser: ' + user.id) return done(null, user.id) }) passport.deserializeUser(function (id, done) { models.User.findOne({ where: { id: id } }).then(function (user) { logger.info('deserializeUser: ' + user.id) return done(null, user) }).catch(function (err) { logger.error(err) return done(err, null) }) }) // check uri is valid before going further app.use(function (req, res, next) { try { decodeURIComponent(req.path) } catch (err) { logger.error(err) return response.errorBadRequest(res) } next() }) // redirect url without trailing slashes app.use(function (req, res, next) { if (req.method === 'GET' && req.path.substr(-1) === '/' && req.path.length > 1) { var query = req.url.slice(req.path.length) var urlpath = req.path.slice(0, -1) var serverurl = config.serverurl if (config.urlpath) serverurl = serverurl.slice(0, -(config.urlpath.length + 1)) res.redirect(301, serverurl + urlpath + query) } else { next() } }) // routes need sessions // template files app.set('views', path.join(__dirname, '/public/views')) // set render engine app.engine('ejs', ejs.renderFile) // set view engine app.set('view engine', 'ejs') // get index app.get('/', response.showIndex) // get 403 forbidden app.get('/403', function (req, res) { response.errorForbidden(res) }) // get 404 not found app.get('/404', function (req, res) { response.errorNotFound(res) }) // get 500 internal error app.get('/500', function (req, res) { response.errorInternalError(res) }) // get status app.get('/status', function (req, res, next) { realtime.getStatus(function (data) { res.set({ 'Cache-Control': 'private', // only cache by client 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling }) res.send(data) }) }) // get status app.get('/temp', function (req, res) { var host = req.get('host') if (config.alloworigin.indexOf(host) === -1) { response.errorForbidden(res) } else { var tempid = req.query.tempid if (!tempid) { response.errorForbidden(res) } else { models.Temp.findOne({ where: { id: tempid } }).then(function (temp) { if (!temp) { response.errorNotFound(res) } else { res.header('Access-Control-Allow-Origin', '*') res.send({ temp: temp.data }) temp.destroy().catch(function (err) { if (err) { logger.error('remove temp failed: ' + err) } }) } }).catch(function (err) { logger.error(err) return response.errorInternalError(res) }) } } }) // post status app.post('/temp', urlencodedParser, function (req, res) { var host = req.get('host') if (config.alloworigin.indexOf(host) === -1) { response.errorForbidden(res) } else { var data = req.body.data if (!data) { response.errorForbidden(res) } else { if (config.debug) { logger.info('SERVER received temp from [' + host + ']: ' + req.body.data) } models.Temp.create({ data: data }).then(function (temp) { if (temp) { res.header('Access-Control-Allow-Origin', '*') res.send({ status: 'ok', id: temp.id }) } else { response.errorInternalError(res) } }).catch(function (err) { logger.error(err) return response.errorInternalError(res) }) } } }) function setReturnToFromReferer (req) { var referer = req.get('referer') if (!req.session) req.session = {} req.session.returnTo = referer } // facebook auth if (config.facebook) { app.get('/auth/facebook', function (req, res, next) { setReturnToFromReferer(req) passport.authenticate('facebook')(req, res, next) }) // facebook auth callback app.get('/auth/facebook/callback', passport.authenticate('facebook', { successReturnToOrRedirect: config.serverurl + '/', failureRedirect: config.serverurl + '/' })) } // twitter auth if (config.twitter) { app.get('/auth/twitter', function (req, res, next) { setReturnToFromReferer(req) passport.authenticate('twitter')(req, res, next) }) // twitter auth callback app.get('/auth/twitter/callback', passport.authenticate('twitter', { successReturnToOrRedirect: config.serverurl + '/', failureRedirect: config.serverurl + '/' })) } // github auth if (config.github) { app.get('/auth/github', function (req, res, next) { setReturnToFromReferer(req) passport.authenticate('github')(req, res, next) }) // github auth callback app.get('/auth/github/callback', passport.authenticate('github', { successReturnToOrRedirect: config.serverurl + '/', failureRedirect: config.serverurl + '/' })) if (!config.gitlab.scope || config.gitlab.scope === 'api') { // gitlab callback actions app.get('/auth/gitlab/callback/:noteId/:action', response.gitlabActions) } } // gitlab auth if (config.gitlab) { app.get('/auth/gitlab', function (req, res, next) { setReturnToFromReferer(req) passport.authenticate('gitlab')(req, res, next) }) // gitlab auth callback app.get('/auth/gitlab/callback', passport.authenticate('gitlab', { successReturnToOrRedirect: config.serverurl + '/', failureRedirect: config.serverurl + '/' })) // gitlab callback actions app.get('/auth/gitlab/callback/:noteId/:action', response.gitlabActions) } // dropbox auth if (config.dropbox) { app.get('/auth/dropbox', function (req, res, next) { setReturnToFromReferer(req) passport.authenticate('dropbox-oauth2')(req, res, next) }) // dropbox auth callback app.get('/auth/dropbox/callback', passport.authenticate('dropbox-oauth2', { successReturnToOrRedirect: config.serverurl + '/', failureRedirect: config.serverurl + '/' })) } // google auth if (config.google) { app.get('/auth/google', function (req, res, next) { setReturnToFromReferer(req) passport.authenticate('google', { scope: ['profile'] })(req, res, next) }) // google auth callback app.get('/auth/google/callback', passport.authenticate('google', { successReturnToOrRedirect: config.serverurl + '/', failureRedirect: config.serverurl + '/' })) } // ldap auth if (config.ldap) { app.post('/auth/ldap', urlencodedParser, function (req, res, next) { if (!req.body.username || !req.body.password) return response.errorBadRequest(res) setReturnToFromReferer(req) passport.authenticate('ldapauth', { successReturnToOrRedirect: config.serverurl + '/', failureRedirect: config.serverurl + '/', failureFlash: true })(req, res, next) }) } // email auth if (config.email) { if (config.allowemailregister) { app.post('/register', urlencodedParser, function (req, res, next) { if (!req.body.email || !req.body.password) return response.errorBadRequest(res) if (!validator.isEmail(req.body.email)) return response.errorBadRequest(res) models.User.findOrCreate({ where: { email: req.body.email }, defaults: { password: req.body.password } }).spread(function (user, created) { if (user) { if (created) { if (config.debug) { logger.info('user registered: ' + user.id) } req.flash('info', "You've successfully registered, please signin.") } else { if (config.debug) { logger.info('user found: ' + user.id) } req.flash('error', 'This email has been used, please try another one.') } return res.redirect(config.serverurl + '/') } req.flash('error', 'Failed to register your account, please try again.') return res.redirect(config.serverurl + '/') }).catch(function (err) { logger.error('auth callback failed: ' + err) return response.errorInternalError(res) }) }) } app.post('/login', urlencodedParser, function (req, res, next) { if (!req.body.email || !req.body.password) return response.errorBadRequest(res) if (!validator.isEmail(req.body.email)) return response.errorBadRequest(res) setReturnToFromReferer(req) passport.authenticate('local', { successReturnToOrRedirect: config.serverurl + '/', failureRedirect: config.serverurl + '/', failureFlash: 'Invalid email or password.' })(req, res, next) }) } // logout app.get('/logout', function (req, res) { if (config.debug && req.isAuthenticated()) { logger.info('user logout: ' + req.user.id) } req.logout() res.redirect(config.serverurl + '/') }) var history = require('./lib/history.js') // get history app.get('/history', history.historyGet) // post history app.post('/history', urlencodedParser, history.historyPost) // post history by note id app.post('/history/:noteId', urlencodedParser, history.historyPost) // delete history app.delete('/history', history.historyDelete) // delete history by note id app.delete('/history/:noteId', history.historyDelete) // get me info app.get('/me', function (req, res) { if (req.isAuthenticated()) { models.User.findOne({ where: { id: req.user.id } }).then(function (user) { if (!user) { return response.errorNotFound(res) } var profile = models.User.getProfile(user) res.send({ status: 'ok', id: req.user.id, name: profile.name, photo: profile.photo }) }).catch(function (err) { logger.error('read me failed: ' + err) return response.errorInternalError(res) }) } else { res.send({ status: 'forbidden' }) } }) // upload image app.post('/uploadimage', function (req, res) { var form = new formidable.IncomingForm() form.keepExtensions = true if (config.imageUploadType === 'filesystem') { form.uploadDir = 'public/uploads' } form.parse(req, function (err, fields, files) { if (err || !files.image || !files.image.path) { response.errorForbidden(res) } else { if (config.debug) { logger.info('SERVER received uploadimage: ' + JSON.stringify(files.image)) } try { switch (config.imageUploadType) { case 'filesystem': res.send({ link: url.resolve(config.serverurl + '/', files.image.path.match(/^public\/(.+$)/)[1]) }) break case 's3': var AWS = require('aws-sdk') var awsConfig = new AWS.Config(config.s3) var s3 = new AWS.S3(awsConfig) fs.readFile(files.image.path, function (err, buffer) { if (err) { logger.error(err) res.status(500).end('upload image error') return } var params = { Bucket: config.s3bucket, Key: path.join('uploads', path.basename(files.image.path)), Body: buffer } s3.putObject(params, function (err, data) { if (err) { logger.error(err) res.status(500).end('upload image error') return } res.send({ link: `https://s3-${config.s3.region}.amazonaws.com/${config.s3bucket}/${params.Key}` }) }) }) break case 'imgur': default: imgur.setClientId(config.imgur.clientID) imgur.uploadFile(files.image.path) .then(function (json) { if (config.debug) { logger.info('SERVER uploadimage success: ' + JSON.stringify(json)) } res.send({ link: json.data.link.replace(/^http:\/\//i, 'https://') }) }) .catch(function (err) { logger.error(err) return res.status(500).end('upload image error') }) break } } catch (err) { logger.error(err) return res.status(500).end('upload image error') } } }) }) // get new note app.get('/new', response.newNote) // get publish note app.get('/s/:shortid', response.showPublishNote) // publish note actions app.get('/s/:shortid/:action', response.publishNoteActions) // get publish slide app.get('/p/:shortid', response.showPublishSlide) // publish slide actions app.get('/p/:shortid/:action', response.publishSlideActions) // get note by id app.get('/:noteId', response.showNote) // note actions app.get('/:noteId/:action', response.noteActions) // note actions with action id app.get('/:noteId/:action/:actionId', response.noteActions) // response not found if no any route matches app.get('*', function (req, res) { response.errorNotFound(res) }) // socket.io secure io.use(realtime.secure) // socket.io auth io.use(passportSocketIo.authorize({ cookieParser: cookieParser, key: config.sessionname, secret: config.sessionsecret, store: sessionStore, success: realtime.onAuthorizeSuccess, fail: realtime.onAuthorizeFail })) // socket.io heartbeat io.set('heartbeat interval', config.heartbeatinterval) io.set('heartbeat timeout', config.heartbeattimeout) // socket.io connection io.sockets.on('connection', realtime.connection) // listen function startListen () { server.listen(config.port, function () { var schema = config.usessl ? 'HTTPS' : 'HTTP' logger.info('%s Server listening at port %d', schema, config.port) config.maintenance = false }) } // sync db then start listen models.sequelize.sync().then(function () { // check if realtime is ready if (realtime.isReady()) { models.Revision.checkAllNotesRevision(function (err, notes) { if (err) throw new Error(err) if (!notes || notes.length <= 0) return startListen() }) } else { throw new Error('server still not ready after db synced') } }) // log uncaught exception process.on('uncaughtException', function (err) { logger.error('An uncaught exception has occured.') logger.error(err) logger.error('Process will exit now.') process.exit(1) }) // install exit handler function handleTermSignals () { config.maintenance = true // disconnect all socket.io clients Object.keys(io.sockets.sockets).forEach(function (key) { var socket = io.sockets.sockets[key] // notify client server going into maintenance status socket.emit('maintenance') setTimeout(function () { socket.disconnect(true) }, 0) }) var checkCleanTimer = setInterval(function () { if (realtime.isReady()) { models.Revision.checkAllNotesRevision(function (err, notes) { if (err) return logger.error(err) if (!notes || notes.length <= 0) { clearInterval(checkCleanTimer) return process.exit(0) } }) } }, 100) } process.on('SIGINT', handleTermSignals) process.on('SIGTERM', handleTermSignals)