563 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			563 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| //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 imgur = require('imgur');
 | |
| var formidable = require('formidable');
 | |
| var morgan = require('morgan');
 | |
| var passportSocketIo = require("passport.socketio");
 | |
| var helmet = require('helmet');
 | |
| var i18n = require('i18n');
 | |
| 
 | |
| //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
 | |
| 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
 | |
|     };
 | |
|     var app = express();
 | |
|     var server = require('https').createServer(options, app);
 | |
| } else {
 | |
|     var app = express();
 | |
|     var server = require('http').createServer(app);
 | |
| }
 | |
| 
 | |
| //logger
 | |
| app.use(morgan('combined', {
 | |
|     "stream": logger.stream
 | |
| }));
 | |
| 
 | |
| //socket io
 | |
| var io = require('socket.io')(server);
 | |
| 
 | |
| //others
 | |
| var realtime = require("./lib/realtime.js");
 | |
| 
 | |
| //assign socket io to realtime
 | |
| realtime.io = io;
 | |
| 
 | |
| //methodOverride
 | |
| app.use(methodOverride('_method'));
 | |
| 
 | |
| // create application/json parser
 | |
| var jsonParser = bodyParser.json({
 | |
|     limit: 1024 * 1024 * 10 // 10 mb
 | |
| });
 | |
| 
 | |
| // 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'],
 | |
|     cookie: 'locale',
 | |
|     directory: __dirname + '/locales'
 | |
| });
 | |
| 
 | |
| app.use(cookieParser());
 | |
| 
 | |
| app.use(i18n.init);
 | |
| 
 | |
| // routes without sessions
 | |
| // static files
 | |
| app.use('/', express.static(__dirname + '/public', { maxAge: config.staticcachetime }));
 | |
| app.use('/vendor/', express.static(__dirname + '/bower_components', { 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();
 | |
|     }
 | |
| });
 | |
| 
 | |
| //passport
 | |
| app.use(passport.initialize());
 | |
| app.use(passport.session());
 | |
| 
 | |
| //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);
 | |
|     });
 | |
| });
 | |
| 
 | |
| // redirect url with trailing slashes
 | |
| app.use(function(req, res, next) {
 | |
|     if ("GET" == req.method && req.path.substr(-1) == '/' && req.path.length > 1) {
 | |
|         var query = req.url.slice(req.path.length);
 | |
|         res.redirect(301, config.serverurl + req.path.slice(0, -1) + query);
 | |
|     } else {
 | |
|         next();
 | |
|     }
 | |
| });
 | |
| 
 | |
| // routes need sessions
 | |
| //template files
 | |
| app.set('views', __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 + '/'
 | |
|         }));
 | |
|     //github callback actions
 | |
|     app.get('/auth/github/callback/:noteId/:action', response.githubActions);
 | |
| }
 | |
| //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 + '/'
 | |
|         }));
 | |
| }
 | |
| //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 + '/');
 | |
| });
 | |
| //get history
 | |
| app.get('/history', function (req, res) {
 | |
|     if (req.isAuthenticated()) {
 | |
|         models.User.findOne({
 | |
|             where: {
 | |
|                 id: req.user.id
 | |
|             }
 | |
|         }).then(function (user) {
 | |
|             if (!user)
 | |
|                 return response.errorNotFound(res);
 | |
|             var history = [];
 | |
|             if (user.history)
 | |
|                 history = JSON.parse(user.history);
 | |
|             res.send({
 | |
|                 history: history
 | |
|             });
 | |
|             if (config.debug)
 | |
|                 logger.info('read history success: ' + user.id);
 | |
|         }).catch(function (err) {
 | |
|             logger.error('read history failed: ' + err);
 | |
|             return response.errorInternalError(res);
 | |
|         });
 | |
|     } else {
 | |
|         return response.errorForbidden(res);
 | |
|     }
 | |
| });
 | |
| //post history
 | |
| app.post('/history', urlencodedParser, function (req, res) {
 | |
|     if (req.isAuthenticated()) {
 | |
|         if (config.debug)
 | |
|             logger.info('SERVER received history from [' + req.user.id + ']: ' + req.body.history);
 | |
|         models.User.update({
 | |
|             history: req.body.history
 | |
|         }, {
 | |
|             where: {
 | |
|                 id: req.user.id
 | |
|             }
 | |
|         }).then(function (count) {
 | |
|             if (!count)
 | |
|                 return response.errorNotFound(res);
 | |
|             if (config.debug)
 | |
|                 logger.info("write user history success: " + req.user.id);
 | |
|         }).catch(function (err) {
 | |
|             logger.error('write history failed: ' + err);
 | |
|             return response.errorInternalError(res);
 | |
|         });
 | |
|         res.end();
 | |
|     } else {
 | |
|         return response.errorForbidden(res);
 | |
|     }
 | |
| });
 | |
| //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.parseProfile(user.profile);
 | |
|             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 to imgur
 | |
| app.post('/uploadimage', function (req, res) {
 | |
|     var form = new formidable.IncomingForm();
 | |
|     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));
 | |
|             imgur.setClientId(config.imgur.clientID);
 | |
|             try {
 | |
|                 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');
 | |
|                     });
 | |
|             } 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) return new Error(err);
 | |
|             if (!notes || notes.length <= 0) return startListen();
 | |
|         });
 | |
|     }
 | |
| });
 | |
| 
 | |
| // 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);
 | |
| });
 | |
| 
 | |
| // gracefully exit
 | |
| process.on('SIGINT', function () {
 | |
|     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');
 | |
|         socket.disconnect(true);
 | |
|     });
 | |
|     var checkCleanTimer = setInterval(function () {
 | |
|         if (realtime.isReady()) {
 | |
|             models.Revision.checkAllNotesRevision(function (err, notes) {
 | |
|                 if (err) return new Error(err);
 | |
|                 if (notes.length <= 0) {
 | |
|                     clearInterval(checkCleanTimer);
 | |
|                     return process.exit(0);
 | |
|                 }
 | |
|             });
 | |
|         }
 | |
|     }, 100);
 | |
| });
 |