274 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			274 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict'
 | |
| // app
 | |
| // external modules
 | |
| var express = require('express')
 | |
| 
 | |
| var ejs = require('ejs')
 | |
| var passport = require('passport')
 | |
| var methodOverride = require('method-override')
 | |
| var cookieParser = require('cookie-parser')
 | |
| var compression = require('compression')
 | |
| var session = require('express-session')
 | |
| var SequelizeStore = require('connect-session-sequelize')(session.Store)
 | |
| var fs = require('fs')
 | |
| var path = require('path')
 | |
| 
 | |
| var morgan = require('morgan')
 | |
| var passportSocketIo = require('passport.socketio')
 | |
| var helmet = require('helmet')
 | |
| var i18n = require('i18n')
 | |
| var flash = require('connect-flash')
 | |
| 
 | |
| // core
 | |
| var config = require('./lib/config')
 | |
| var logger = require('./lib/logger')
 | |
| var response = require('./lib/response')
 | |
| var models = require('./lib/models')
 | |
| var csp = require('./lib/csp')
 | |
| 
 | |
| // 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
 | |
| }))
 | |
| 
 | |
| // 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'))
 | |
| 
 | |
| // session store
 | |
| var sessionStore = new SequelizeStore({
 | |
|   db: models.sequelize
 | |
| })
 | |
| 
 | |
| // compression
 | |
| app.use(compression())
 | |
| 
 | |
| // use hsts to tell https users stick to this
 | |
| if (config.hsts.enable) {
 | |
|   app.use(helmet.hsts({
 | |
|     maxAge: config.hsts.maxAgeSeconds * 1000,
 | |
|     includeSubdomains: config.hsts.includeSubdomains,
 | |
|     preload: config.hsts.preload
 | |
|   }))
 | |
| } else if (config.useSSL) {
 | |
|   logger.info('Consider enabling HSTS for extra security:')
 | |
|   logger.info('https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security')
 | |
| }
 | |
| 
 | |
| // Add referrer policy to improve privacy
 | |
| app.use(
 | |
|   helmet.referrerPolicy({
 | |
|     policy: 'same-origin'
 | |
|   })
 | |
| )
 | |
| 
 | |
| // Generate a random nonce per request, for CSP with inline scripts
 | |
| app.use(csp.addNonceToLocals)
 | |
| 
 | |
| // use Content-Security-Policy to limit XSS, dangerous plugins, etc.
 | |
| // https://helmetjs.github.io/docs/csp/
 | |
| if (config.csp.enable) {
 | |
|   app.use(helmet.contentSecurityPolicy({
 | |
|     directives: csp.computeDirectives()
 | |
|   }))
 | |
| } else {
 | |
|   logger.info('Content-Security-Policy is disabled. This may be a security risk.')
 | |
| }
 | |
| 
 | |
| i18n.configure({
 | |
|   locales: ['en', 'zh-CN', 'zh-TW', 'fr', 'de', 'ja', 'es', 'ca', 'el', 'pt', 'it', 'tr', 'ru', 'nl', 'hr', 'pl', 'uk', 'hi', 'sv', 'eo', 'da', 'ko'],
 | |
|   cookie: 'locale',
 | |
|   directory: path.join(__dirname, '/locales'),
 | |
|   updateFiles: config.updateI18nFiles
 | |
| })
 | |
| 
 | |
| 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(require('./lib/web/middleware/tooBusy'))
 | |
| 
 | |
| app.use(flash())
 | |
| 
 | |
| // passport
 | |
| app.use(passport.initialize())
 | |
| app.use(passport.session())
 | |
| 
 | |
| // check uri is valid before going further
 | |
| app.use(require('./lib/web/middleware/checkURIValid'))
 | |
| // redirect url without trailing slashes
 | |
| app.use(require('./lib/web/middleware/redirectWithoutTrailingSlashes'))
 | |
| app.use(require('./lib/web/middleware/codiMDVersion'))
 | |
| 
 | |
| // 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')
 | |
| 
 | |
| app.use(require('./lib/web/baseRouter'))
 | |
| app.use(require('./lib/web/statusRouter'))
 | |
| app.use(require('./lib/web/auth'))
 | |
| app.use(require('./lib/web/historyRouter'))
 | |
| app.use(require('./lib/web/userRouter'))
 | |
| app.use(require('./lib/web/imageRouter'))
 | |
| app.use(require('./lib/web/noteRouter'))
 | |
| 
 | |
| // response not found if no any route matxches
 | |
| 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 () {
 | |
|   var address
 | |
|   var listenCallback = function () {
 | |
|     var schema = config.useSSL ? 'HTTPS' : 'HTTP'
 | |
|     logger.info('%s Server listening at %s', schema, address)
 | |
|     realtime.maintenance = false
 | |
|   }
 | |
| 
 | |
|   // use unix domain socket if 'path' is specified
 | |
|   if (config.path) {
 | |
|     address = config.path
 | |
|     server.listen(config.path, listenCallback)
 | |
|   } else {
 | |
|     address = config.host + ':' + config.port
 | |
|     server.listen(config.port, config.host, listenCallback)
 | |
|   }
 | |
| }
 | |
| 
 | |
| // 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 () {
 | |
|   logger.info('CodiMD has been killed by signal, try to exit gracefully...')
 | |
|   realtime.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)
 | |
| process.on('SIGQUIT', handleTermSignals)
 |