2020-04-12 14:52:28 -04:00
|
|
|
import compression from 'compression'
|
2020-04-18 13:10:17 -04:00
|
|
|
import flash from 'connect-flash'
|
2020-04-12 14:52:28 -04:00
|
|
|
// eslint-disable-next-line @typescript-eslint/camelcase
|
|
|
|
import connect_session_sequelize from 'connect-session-sequelize'
|
2020-04-18 13:10:17 -04:00
|
|
|
import cookieParser from 'cookie-parser'
|
|
|
|
import ejs from 'ejs'
|
|
|
|
import express from 'express'
|
|
|
|
import session from 'express-session'
|
2020-06-20 10:37:31 -04:00
|
|
|
import childProcess from 'child_process'
|
2020-04-18 13:10:17 -04:00
|
|
|
import helmet from 'helmet'
|
|
|
|
import http from 'http'
|
|
|
|
import https from 'https'
|
|
|
|
import i18n from 'i18n'
|
2020-04-12 14:52:28 -04:00
|
|
|
import fs from 'fs'
|
2020-04-18 13:10:17 -04:00
|
|
|
import methodOverride from 'method-override'
|
2020-04-12 14:52:28 -04:00
|
|
|
import morgan from 'morgan'
|
2020-04-18 13:10:17 -04:00
|
|
|
import passport from 'passport'
|
2020-04-12 14:52:28 -04:00
|
|
|
import passportSocketIo from 'passport.socketio'
|
2020-04-18 13:10:17 -04:00
|
|
|
import path from 'path'
|
|
|
|
import SocketIO from 'socket.io'
|
|
|
|
import WebSocket from 'ws'
|
2020-04-12 14:52:28 -04:00
|
|
|
|
2020-04-12 07:11:06 -04:00
|
|
|
import { config } from './config'
|
2020-04-12 14:52:28 -04:00
|
|
|
import { addNonceToLocals, computeDirectives } from './csp'
|
2020-04-18 13:10:17 -04:00
|
|
|
import { errors } from './errors'
|
|
|
|
import { logger } from './logger'
|
2020-06-02 07:48:38 -04:00
|
|
|
import { Revision, sequelize, runMigrations } from './models'
|
Improve handling of termination signals
Previously, upon receiving a termination signal, the process tries to
flush all changes to the database, retrying every 0.1s until it
succeeds. However, if the database is not set up properly, this always
fails, and spams the terminal/logging with the error message 10 times a
second.
If the user sends another termination signal, the handleTermSignal
function is called once again, and we get twice the number of error
messages.
This commit changes the behaviour in various ways.
(1) It lowers the retry rate to 0.5s, and aborts after 30 seconds.
(2) If the write to the database errored, the error message explains
that this is due to us flushing the final changes.
(3) We replace realtime.maintenance with realtime.state, which is an
Enum with three possible states --- Starting, Running, and Stopping.
If a termination signal is received in the starting state, the
process simply aborts because there is nothing to clean up. This is
the case when the database is misconfigured, since the application
starts up only after connecting to the databse. If it is in the
Stopping state, the handleTermSignal function returns because
another instance of handleTermSignal is already running.
Fixes #408
Signed-off-by: Dexter Chua <dec41@srcf.net>
2020-06-26 23:06:48 -04:00
|
|
|
import { realtime, State } from './realtime'
|
2020-06-12 16:15:04 -04:00
|
|
|
import { handleTermSignals } from './utils/functions'
|
2020-07-24 15:06:55 -04:00
|
|
|
import { AuthRouter, BaseRouter, HistoryRouter, ImageRouter, NoteRouter, StatusRouter, UserRouter } from './web'
|
2020-04-13 08:58:23 -04:00
|
|
|
import { tooBusy, checkURI, redirectWithoutTrailingSlashes, codiMDVersion } from './web/middleware'
|
|
|
|
|
|
|
|
const rootPath = path.join(__dirname, '..')
|
2020-04-18 13:37:47 -04:00
|
|
|
|
|
|
|
// session store
|
|
|
|
const SequelizeStore = connect_session_sequelize(session.Store)
|
|
|
|
|
|
|
|
const sessionStore = new SequelizeStore({
|
|
|
|
db: sequelize
|
|
|
|
})
|
|
|
|
|
2017-03-08 05:45:51 -05:00
|
|
|
// server setup
|
2020-04-12 07:11:06 -04:00
|
|
|
const app = express()
|
2020-04-18 13:37:47 -04:00
|
|
|
let server: http.Server
|
2018-03-07 09:17:35 -05:00
|
|
|
if (config.useSSL) {
|
2020-04-18 13:11:03 -04:00
|
|
|
const ca: string[] = []
|
|
|
|
for (const path of config.sslCAPath) {
|
|
|
|
ca.push(fs.readFileSync(path, 'utf8'))
|
|
|
|
}
|
|
|
|
|
2020-04-11 05:24:53 -04:00
|
|
|
const options = {
|
2018-03-07 09:17:35 -05:00
|
|
|
key: fs.readFileSync(config.sslKeyPath, 'utf8'),
|
|
|
|
cert: fs.readFileSync(config.sslCertPath, 'utf8'),
|
2017-03-08 05:45:51 -05:00
|
|
|
ca: ca,
|
2018-03-07 09:17:35 -05:00
|
|
|
dhparam: fs.readFileSync(config.dhParamPath, 'utf8'),
|
2017-03-08 05:45:51 -05:00
|
|
|
requestCert: false,
|
2020-04-18 11:56:50 -04:00
|
|
|
rejectUnauthorized: false,
|
|
|
|
heartbeatInterval: config.heartbeatInterval,
|
|
|
|
heartbeatTimeout: config.heartbeatTimeout
|
2020-04-12 07:11:06 -04:00
|
|
|
}
|
2020-04-18 11:56:50 -04:00
|
|
|
server = https.createServer(options, app)
|
2015-05-15 00:58:13 -04:00
|
|
|
} else {
|
2020-04-18 11:56:50 -04:00
|
|
|
server = http.createServer(app)
|
2015-05-15 00:58:13 -04:00
|
|
|
}
|
2015-07-01 12:10:20 -04:00
|
|
|
|
2020-06-10 06:21:11 -04:00
|
|
|
// if we manage to provide HTTPS domains, but don't provide TLS ourselves
|
|
|
|
// obviously a proxy is involded. In order to make sure express is aware of
|
|
|
|
// this, we provide the option to trust proxies here.
|
|
|
|
if (!config.useSSL && config.protocolUseSSL) {
|
|
|
|
app.set('trust proxy', 1)
|
|
|
|
}
|
|
|
|
|
2017-03-08 05:45:51 -05:00
|
|
|
// socket io
|
2020-06-08 09:29:27 -04:00
|
|
|
const io = SocketIO(server, { cookie: false })
|
2020-04-18 11:56:50 -04:00
|
|
|
io.engine.ws = new WebSocket.Server({
|
2017-03-08 05:45:51 -05:00
|
|
|
noServer: true,
|
|
|
|
perMessageDeflate: false
|
|
|
|
})
|
|
|
|
// assign socket io to realtime
|
|
|
|
realtime.io = io
|
2015-09-23 23:36:41 -04:00
|
|
|
|
2020-04-18 13:37:47 -04:00
|
|
|
// 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 connection
|
|
|
|
io.sockets.on('connection', realtime.connection)
|
2015-06-01 06:04:25 -04:00
|
|
|
|
2020-04-18 13:37:47 -04:00
|
|
|
// logger
|
|
|
|
app.use(morgan('combined', {
|
|
|
|
stream: {
|
|
|
|
write: function (message): void {
|
|
|
|
logger.info(message)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}))
|
2015-05-04 03:53:29 -04:00
|
|
|
|
2016-03-14 22:41:49 -04:00
|
|
|
// use hsts to tell https users stick to this
|
2017-10-12 19:09:04 -04:00
|
|
|
if (config.hsts.enable) {
|
|
|
|
app.use(helmet.hsts({
|
2018-11-19 16:01:43 -05:00
|
|
|
maxAge: config.hsts.maxAgeSeconds,
|
2017-10-12 19:09:04 -04:00
|
|
|
includeSubdomains: config.hsts.includeSubdomains,
|
|
|
|
preload: config.hsts.preload
|
|
|
|
}))
|
2018-03-07 09:17:35 -05:00
|
|
|
} else if (config.useSSL) {
|
2017-10-12 19:09:04 -04:00
|
|
|
logger.info('Consider enabling HSTS for extra security:')
|
|
|
|
logger.info('https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security')
|
|
|
|
}
|
2016-03-14 22:41:49 -04:00
|
|
|
|
2017-10-21 19:22:48 -04:00
|
|
|
// Generate a random nonce per request, for CSP with inline scripts
|
2020-04-12 14:52:28 -04:00
|
|
|
app.use(addNonceToLocals)
|
2017-10-18 11:48:53 -04:00
|
|
|
|
2017-10-18 11:10:23 -04:00
|
|
|
// use Content-Security-Policy to limit XSS, dangerous plugins, etc.
|
|
|
|
// https://helmetjs.github.io/docs/csp/
|
|
|
|
if (config.csp.enable) {
|
|
|
|
app.use(helmet.contentSecurityPolicy({
|
2020-04-12 14:52:28 -04:00
|
|
|
directives: computeDirectives()
|
2017-10-18 11:10:23 -04:00
|
|
|
}))
|
|
|
|
} else {
|
2017-10-18 13:37:55 -04:00
|
|
|
logger.info('Content-Security-Policy is disabled. This may be a security risk.')
|
2017-10-18 11:10:23 -04:00
|
|
|
}
|
|
|
|
|
2020-04-18 13:37:47 -04:00
|
|
|
// Add referrer policy to improve privacy
|
|
|
|
app.use(
|
|
|
|
helmet.referrerPolicy({
|
|
|
|
policy: 'same-origin'
|
|
|
|
})
|
|
|
|
)
|
|
|
|
|
|
|
|
// methodOverride
|
|
|
|
app.use(methodOverride('_method'))
|
|
|
|
|
|
|
|
// compression
|
|
|
|
app.use(compression())
|
|
|
|
|
|
|
|
app.use(cookieParser())
|
|
|
|
|
2016-08-18 23:49:24 -04:00
|
|
|
i18n.configure({
|
2019-10-05 16:32:47 -04:00
|
|
|
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', 'id', 'sr', 'vi', 'ar', 'cs', 'sk'],
|
2017-03-08 05:45:51 -05:00
|
|
|
cookie: 'locale',
|
2019-04-04 06:31:08 -04:00
|
|
|
indent: ' ', // this is the style poeditor.com exports it, this creates less churn
|
2020-04-13 08:58:23 -04:00
|
|
|
directory: path.resolve(rootPath, config.localesPath),
|
2018-06-04 19:29:27 -04:00
|
|
|
updateFiles: config.updateI18nFiles
|
2017-03-08 05:45:51 -05:00
|
|
|
})
|
2016-08-18 23:49:24 -04:00
|
|
|
|
2017-03-08 05:45:51 -05:00
|
|
|
app.use(i18n.init)
|
2016-08-18 23:49:24 -04:00
|
|
|
|
2020-04-18 13:37:47 -04:00
|
|
|
// set generally available variables for all views
|
|
|
|
app.locals.useCDN = config.useCDN
|
|
|
|
app.locals.serverURL = config.serverURL
|
|
|
|
app.locals.sourceURL = config.sourceURL
|
|
|
|
app.locals.allowAnonymous = config.allowAnonymous
|
|
|
|
app.locals.allowAnonymousEdits = config.allowAnonymousEdits
|
|
|
|
app.locals.authProviders = {
|
|
|
|
facebook: config.isFacebookEnable,
|
|
|
|
twitter: config.isTwitterEnable,
|
|
|
|
github: config.isGitHubEnable,
|
|
|
|
gitlab: config.isGitLabEnable,
|
|
|
|
dropbox: config.isDropboxEnable,
|
|
|
|
google: config.isGoogleEnable,
|
|
|
|
ldap: config.isLDAPEnable,
|
|
|
|
ldapProviderName: config.ldap.providerName,
|
|
|
|
saml: config.isSAMLEnable,
|
|
|
|
oauth2: config.isOAuth2Enable,
|
|
|
|
oauth2ProviderName: config.oauth2.providerName,
|
|
|
|
openID: config.isOpenIDEnable,
|
|
|
|
email: config.isEmailEnable,
|
|
|
|
allowEmailRegister: config.allowEmailRegister
|
|
|
|
}
|
|
|
|
|
|
|
|
// Export/Import menu items
|
|
|
|
app.locals.enableDropBoxSave = config.isDropboxEnable
|
|
|
|
app.locals.enableGitHubGist = config.isGitHubEnable
|
|
|
|
app.locals.enableGitlabSnippets = config.isGitlabSnippetsEnable
|
2016-04-20 06:14:28 -04:00
|
|
|
|
2017-03-08 05:45:51 -05:00
|
|
|
// session
|
2015-05-04 03:53:29 -04:00
|
|
|
app.use(session({
|
2018-03-07 09:17:35 -05:00
|
|
|
name: config.sessionName,
|
|
|
|
secret: config.sessionSecret,
|
2017-03-08 05:45:51 -05:00
|
|
|
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: {
|
2020-06-08 09:27:31 -04:00
|
|
|
maxAge: config.sessionLife,
|
2020-06-10 09:08:39 -04:00
|
|
|
sameSite: 'lax',
|
2020-06-08 09:11:17 -04:00
|
|
|
secure: config.useSSL || config.protocolUseSSL || false
|
2017-03-08 05:45:51 -05:00
|
|
|
},
|
|
|
|
store: sessionStore
|
|
|
|
}))
|
2015-05-04 03:53:29 -04:00
|
|
|
|
2016-03-14 22:42:07 -04:00
|
|
|
// session resumption
|
2020-04-12 07:11:06 -04:00
|
|
|
const tlsSessionStore = {}
|
2016-03-14 22:42:07 -04:00
|
|
|
server.on('newSession', function (id, data, cb) {
|
2017-03-08 05:45:51 -05:00
|
|
|
tlsSessionStore[id.toString('hex')] = data
|
|
|
|
cb()
|
|
|
|
})
|
2016-03-14 22:42:07 -04:00
|
|
|
server.on('resumeSession', function (id, cb) {
|
2017-03-08 05:45:51 -05:00
|
|
|
cb(null, tlsSessionStore[id.toString('hex')] || null)
|
|
|
|
})
|
2016-03-14 22:42:07 -04:00
|
|
|
|
2017-03-08 05:45:51 -05:00
|
|
|
// middleware which blocks requests when we're too busy
|
2020-04-13 08:58:23 -04:00
|
|
|
app.use(tooBusy)
|
2015-05-04 03:53:29 -04:00
|
|
|
|
2017-03-08 05:45:51 -05:00
|
|
|
app.use(flash())
|
2016-12-01 12:58:14 -05:00
|
|
|
|
2017-03-08 05:45:51 -05:00
|
|
|
// passport
|
|
|
|
app.use(passport.initialize())
|
|
|
|
app.use(passport.session())
|
2015-05-04 03:53:29 -04:00
|
|
|
|
2016-12-03 01:37:24 -05:00
|
|
|
// check uri is valid before going further
|
2020-04-13 08:58:23 -04:00
|
|
|
app.use(checkURI)
|
2016-12-11 21:50:43 -05:00
|
|
|
// redirect url without trailing slashes
|
2020-04-13 08:58:23 -04:00
|
|
|
app.use(redirectWithoutTrailingSlashes)
|
|
|
|
app.use(codiMDVersion)
|
2016-04-20 06:19:11 -04:00
|
|
|
|
2020-04-18 13:37:47 -04:00
|
|
|
// routes without sessions
|
|
|
|
// static files
|
|
|
|
app.use('/', express.static(path.resolve(rootPath, config.publicPath), { maxAge: config.staticCacheTime, index: false, redirect: false }))
|
|
|
|
app.use('/docs', express.static(path.resolve(rootPath, config.docsPath), { maxAge: config.staticCacheTime, redirect: false }))
|
|
|
|
app.use('/uploads', express.static(path.resolve(rootPath, config.uploadsPath), { maxAge: config.staticCacheTime, redirect: false }))
|
|
|
|
app.use('/default.md', express.static(path.resolve(rootPath, config.defaultNotePath), { maxAge: config.staticCacheTime }))
|
|
|
|
|
2016-04-20 06:14:28 -04:00
|
|
|
// routes need sessions
|
2017-03-08 05:45:51 -05:00
|
|
|
// template files
|
2018-09-10 16:35:38 -04:00
|
|
|
app.set('views', config.viewPath)
|
2017-03-08 05:45:51 -05:00
|
|
|
// set render engine
|
|
|
|
app.engine('ejs', ejs.renderFile)
|
|
|
|
// set view engine
|
|
|
|
app.set('view engine', 'ejs')
|
2016-07-31 12:06:07 -04:00
|
|
|
|
2020-04-12 14:52:28 -04:00
|
|
|
app.use(BaseRouter)
|
|
|
|
app.use(StatusRouter)
|
2020-04-13 05:44:21 -04:00
|
|
|
app.use(AuthRouter)
|
2020-04-12 14:52:28 -04:00
|
|
|
app.use(HistoryRouter)
|
2020-04-13 05:36:23 -04:00
|
|
|
app.use(UserRouter)
|
2020-04-12 14:52:28 -04:00
|
|
|
app.use(ImageRouter)
|
|
|
|
app.use(NoteRouter)
|
2017-04-11 17:56:20 -04:00
|
|
|
|
|
|
|
// response not found if no any route matxches
|
2016-04-20 06:19:29 -04:00
|
|
|
app.get('*', function (req, res) {
|
2019-10-27 08:51:53 -04:00
|
|
|
errors.errorNotFound(res)
|
2017-03-08 05:45:51 -05:00
|
|
|
})
|
2015-05-04 03:53:29 -04:00
|
|
|
|
2020-04-18 13:37:47 -04:00
|
|
|
// 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)
|
|
|
|
})
|
2017-03-08 05:45:51 -05:00
|
|
|
|
|
|
|
// listen
|
2020-04-18 11:56:50 -04:00
|
|
|
function startListen (): void {
|
2020-04-12 07:11:06 -04:00
|
|
|
let address
|
2020-04-18 11:56:50 -04:00
|
|
|
const listenCallback = function (): void {
|
2020-04-12 07:11:06 -04:00
|
|
|
const schema = config.useSSL ? 'HTTPS' : 'HTTP'
|
2018-07-22 20:27:51 -04:00
|
|
|
logger.info('%s Server listening at %s', schema, address)
|
Improve handling of termination signals
Previously, upon receiving a termination signal, the process tries to
flush all changes to the database, retrying every 0.1s until it
succeeds. However, if the database is not set up properly, this always
fails, and spams the terminal/logging with the error message 10 times a
second.
If the user sends another termination signal, the handleTermSignal
function is called once again, and we get twice the number of error
messages.
This commit changes the behaviour in various ways.
(1) It lowers the retry rate to 0.5s, and aborts after 30 seconds.
(2) If the write to the database errored, the error message explains
that this is due to us flushing the final changes.
(3) We replace realtime.maintenance with realtime.state, which is an
Enum with three possible states --- Starting, Running, and Stopping.
If a termination signal is received in the starting state, the
process simply aborts because there is nothing to clean up. This is
the case when the database is misconfigured, since the application
starts up only after connecting to the databse. If it is in the
Stopping state, the handleTermSignal function returns because
another instance of handleTermSignal is already running.
Fixes #408
Signed-off-by: Dexter Chua <dec41@srcf.net>
2020-06-26 23:06:48 -04:00
|
|
|
realtime.state = State.Running
|
2020-04-12 07:11:06 -04:00
|
|
|
}
|
2018-07-22 20:27:51 -04:00
|
|
|
|
2020-06-20 10:37:31 -04:00
|
|
|
const unixCallback = function (): void {
|
|
|
|
const throwErr = function (err): void { if (err) throw err }
|
|
|
|
if (config.socket.owner !== undefined) {
|
|
|
|
childProcess.spawn('chown', [config.socket.owner, config.path]).on('error', throwErr)
|
|
|
|
}
|
|
|
|
if (config.socket.group !== undefined) {
|
|
|
|
childProcess.spawn('chgrp', [config.socket.group, config.path]).on('error', throwErr)
|
|
|
|
}
|
|
|
|
if (config.socket.mode !== undefined) {
|
|
|
|
fs.chmod(config.path, config.socket.mode, throwErr)
|
|
|
|
}
|
|
|
|
listenCallback()
|
|
|
|
}
|
2018-07-22 20:27:51 -04:00
|
|
|
// use unix domain socket if 'path' is specified
|
|
|
|
if (config.path) {
|
|
|
|
address = config.path
|
2020-06-20 10:37:31 -04:00
|
|
|
server.listen(config.path, unixCallback)
|
2018-07-22 20:27:51 -04:00
|
|
|
} else {
|
|
|
|
address = config.host + ':' + config.port
|
|
|
|
server.listen(config.port, config.host, listenCallback)
|
|
|
|
}
|
2015-07-11 00:44:16 -04:00
|
|
|
}
|
2016-04-20 06:03:55 -04:00
|
|
|
|
|
|
|
// sync db then start listen
|
2020-06-02 07:48:38 -04:00
|
|
|
sequelize.authenticate().then(async function () {
|
|
|
|
await runMigrations()
|
2020-04-25 08:38:15 -04:00
|
|
|
sessionStore.sync()
|
2017-03-08 05:45:51 -05:00
|
|
|
// check if realtime is ready
|
|
|
|
if (realtime.isReady()) {
|
2020-04-10 16:06:12 -04:00
|
|
|
Revision.checkAllNotesRevision(function (err, notes) {
|
2020-04-18 11:56:50 -04:00
|
|
|
if (err) {
|
|
|
|
throw new Error(err)
|
|
|
|
}
|
|
|
|
if (!notes || notes.length <= 0) {
|
|
|
|
return startListen()
|
|
|
|
}
|
2017-03-08 05:45:51 -05:00
|
|
|
})
|
|
|
|
} else {
|
|
|
|
throw new Error('server still not ready after db synced')
|
|
|
|
}
|
|
|
|
})
|
2016-04-20 06:03:55 -04:00
|
|
|
|
2020-04-18 13:37:47 -04:00
|
|
|
process.on('SIGINT', () => handleTermSignals(io))
|
|
|
|
process.on('SIGTERM', () => handleTermSignals(io))
|
|
|
|
process.on('SIGQUIT', () => handleTermSignals(io))
|