HackMD/app.js

522 lines
16 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 history = require("./lib/history.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', 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.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 (history.isReady() && realtime.isReady()) {
models.Revision.checkAllNotesRevision(function (err, notes) {
if (err) throw 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 (history.isReady() && realtime.isReady()) {
models.Revision.checkAllNotesRevision(function (err, notes) {
if (err) throw new Error(err);
if (!notes || notes.length <= 0) {
clearInterval(checkCleanTimer);
return process.exit(0);
}
});
}
}, 100);
});