hedgedoc/app.js

374 lines
11 KiB
JavaScript
Raw Normal View History

2017-04-12 13:16:33 -04:00
'use strict'
// app
// external modules
const express = require('express')
const ejs = require('ejs')
const passport = require('passport')
const methodOverride = require('method-override')
const cookieParser = require('cookie-parser')
const compression = require('compression')
const session = require('express-session')
const SequelizeStore = require('connect-session-sequelize')(session.Store)
const fs = require('fs')
const path = require('path')
const morgan = require('morgan')
const passportSocketIo = require('passport.socketio')
const helmet = require('helmet')
const i18n = require('i18n')
const flash = require('connect-flash')
const apiMetrics = require('prometheus-api-metrics')
// core
const config = require('./lib/config')
const logger = require('./lib/logger')
const errors = require('./lib/errors')
const models = require('./lib/models')
const csp = require('./lib/csp')
const metrics = require('./lib/prometheus')
const { useUnless } = require('./lib/utils')
const supportedLocalesList = Object.keys(require('./locales/_supported.json'))
// server setup
const app = express()
let server = null
if (config.useSSL) {
const ca = (function () {
let i, len
const results = []
for (i = 0, len = config.sslCAPath.length; i < len; i++) {
results.push(fs.readFileSync(config.sslCAPath[i], 'utf8'))
}
return results
})()
const options = {
key: fs.readFileSync(config.sslKeyPath, 'utf8'),
cert: fs.readFileSync(config.sslCertPath, 'utf8'),
ca,
dhparam: fs.readFileSync(config.dhParamPath, 'utf8'),
requestCert: false,
rejectUnauthorized: false
}
server = require('https').createServer(options, app)
2015-05-15 00:58:13 -04:00
} else {
server = require('http').createServer(app)
2015-05-15 00:58:13 -04:00
}
2015-07-01 12:10:20 -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)
}
// check if the request is from container healthcheck
function isContainerHealthCheck (req, _) {
return req.headers['user-agent'] === 'hedgedoc-container-healthcheck/1.0' && req.ip === '127.0.0.1'
}
// logger
2015-06-01 06:04:25 -04:00
app.use(morgan('combined', {
skip: isContainerHealthCheck,
stream: logger.stream
}))
2015-05-04 03:53:29 -04:00
// Register prometheus metrics endpoint
if (config.enableStatsApi) {
app.use(apiMetrics())
metrics.setupCustomPrometheusMetrics()
}
// socket io
const io = require('socket.io')(server, { cookie: false })
2015-07-01 12:10:20 -04:00
// others
const realtime = require('./lib/realtime.js')
2015-05-04 03:53:29 -04:00
// assign socket io to realtime
realtime.io = io
// methodOverride
app.use(methodOverride('_method'))
2015-05-04 03:53:29 -04:00
// session store
const sessionStore = new SequelizeStore({
db: models.sequelize
})
2015-06-01 06:04:25 -04:00
// compression
app.use(compression())
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
if (config.hsts.enable) {
app.use(helmet.hsts({
maxAge: config.hsts.maxAgeSeconds,
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')
}
2016-03-14 22:41:49 -04:00
// 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)
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({
directives: csp.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
}
i18n.configure({
locales: supportedLocalesList,
cookie: 'locale',
indent: ' ', // this is the style poeditor.com exports it, this creates less churn
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, index: false, redirect: false }))
app.use('/docs', express.static(path.resolve(__dirname, config.docsPath), { maxAge: config.staticCacheTime, redirect: false }))
app.use('/uploads', express.static(path.resolve(__dirname, config.uploadsPath), { maxAge: config.staticCacheTime, redirect: false }))
app.use('/default.md', express.static(path.resolve(__dirname, config.defaultNotePath), { maxAge: config.staticCacheTime }))
// session
app.use(useUnless(['/status', '/metrics', '/_health'], 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,
sameSite: config.cookiePolicy, // be careful: setting a SameSite value of none without https breaks the editor
secure: config.useSSL || config.protocolUseSSL || false
},
store: sessionStore
})))
2015-05-04 03:53:29 -04:00
// session resumption
const 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
2017-04-11 18:05:43 -04:00
app.use(require('./lib/web/middleware/tooBusy'))
2015-05-04 03:53:29 -04:00
app.use(flash())
// passport
app.use(passport.initialize())
app.use(useUnless(['/status', '/metrics', '/_health'], passport.session()))
2015-05-04 03:53:29 -04:00
// check uri is valid before going further
app.use(require('./lib/web/middleware/checkURIValid'))
// redirect url without trailing slashes
2017-05-07 12:52:13 -04:00
app.use(require('./lib/web/middleware/redirectWithoutTrailingSlashes'))
Replace CodiMD with HedgeDoc Signed-off-by: Erik Michelson <github@erik.michelson.eu> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: References in public/views Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Update links in README Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Update links in SECURITY.md Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Update links in LICENSE Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Update links in docs/configuration.md Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Update links in bin/setup Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: References in docs/guides Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: References in docs/dev Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: References in docs/guides/auth Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: References in docs/setup Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Update various links in code to the new GitHub org. Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: codiMDVersion.js is now hedgeDocVersion.js Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: References in docs/setup/yunohost Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rebrand to HedgeDoc: Add banner and logo Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Update links in docs/guides/migrate-etherpad Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Remove note in docs/guides/auth/github Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Replace links in public/docs/features Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Add todo placeholder in docs/history Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Replace github link in public/views/index/body Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Replace github link in README Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Add logo to README Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Add note about the renaming to the front page Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Removed Travis from README.md and change CodiMD to HedgeDoc in some places Signed-off-by: Yannick Bungers <git@innay.de> Some more renaming to HedgeDoc - Fixed capitalization of HedgeDoc - Added renaming for etherpad migration doc Signed-off-by: Yannick Bungers <git@innay.de> Changed Repo name to hedgedoc Signed-off-by: Yannick Bungers <git@innay.de>
2020-07-02 11:22:52 -04:00
app.use(require('./lib/web/middleware/hedgeDocVersion'))
// routes need sessions
// template files
app.set('views', config.viewPath)
// set render engine
app.engine('ejs', ejs.renderFile)
// set view engine
app.set('view engine', 'ejs')
// set generally available variables for all views
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,
mattermost: config.isMattermostEnable,
dropbox: config.isDropboxEnable,
google: config.isGoogleEnable,
ldap: config.isLDAPEnable,
ldapProviderName: config.ldap.providerName,
saml: config.isSAMLEnable,
samlProviderName: config.saml.providerName,
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
app.use(require('./lib/web/baseRouter'))
app.use(require('./lib/web/statusRouter'))
app.use(require('./lib/web/auth'))
2017-04-11 17:48:55 -04:00
app.use(require('./lib/web/historyRouter'))
2017-04-11 17:55:36 -04:00
app.use(require('./lib/web/userRouter'))
2017-04-11 18:01:45 -04:00
app.use(require('./lib/web/imageRouter'))
app.use(require('./lib/web/note/router'))
2017-04-11 17:56:20 -04:00
// response not found if no any route matxches
app.get('*', function (req, res) {
errors.errorNotFound(res)
})
2015-05-04 03:53:29 -04:00
// socket.io secure
io.use(realtime.secure)
// socket.io auth
2015-06-01 06:04:25 -04:00
io.use(passportSocketIo.authorize({
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 () {
let address
const listenCallback = function () {
const 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)
}
}
const maxDBTries = 30
let currentDBTry = 1
function syncAndListen () {
// sync db then start listen
models.sequelize.authenticate().then(function () {
models.runMigrations().then(() => {
sessionStore.sync()
// 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 {
logger.error('server still not ready after db synced')
process.exit(1)
}
})
}).catch((dbError) => {
if (currentDBTry < maxDBTries) {
logger.warn(`Database cannot be reached. Try ${currentDBTry} of ${maxDBTries}. (${dbError})`)
currentDBTry++
setTimeout(function () {
syncAndListen()
}, 1000)
} else {
logger.error('Cannot reach database! Exiting.')
process.exit(1)
}
})
}
syncAndListen()
// 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)
})
let alreadyHandlingTermSignals = false
// install exit handler
function handleTermSignals () {
if (alreadyHandlingTermSignals) {
logger.info('Forcefully exiting.')
process.exit(1)
}
Replace CodiMD with HedgeDoc Signed-off-by: Erik Michelson <github@erik.michelson.eu> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: References in public/views Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Update links in README Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Update links in SECURITY.md Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Update links in LICENSE Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Update links in docs/configuration.md Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Update links in bin/setup Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: References in docs/guides Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: References in docs/dev Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: References in docs/guides/auth Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: References in docs/setup Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Update various links in code to the new GitHub org. Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: codiMDVersion.js is now hedgeDocVersion.js Signed-off-by: David Mehren <git@herrmehren.de> Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: References in docs/setup/yunohost Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rebrand to HedgeDoc: Add banner and logo Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Update links in docs/guides/migrate-etherpad Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Remove note in docs/guides/auth/github Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Replace links in public/docs/features Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Add todo placeholder in docs/history Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Replace github link in public/views/index/body Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Replace github link in README Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Add logo to README Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Rename to HedgeDoc: Add note about the renaming to the front page Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Removed Travis from README.md and change CodiMD to HedgeDoc in some places Signed-off-by: Yannick Bungers <git@innay.de> Some more renaming to HedgeDoc - Fixed capitalization of HedgeDoc - Added renaming for etherpad migration doc Signed-off-by: Yannick Bungers <git@innay.de> Changed Repo name to hedgedoc Signed-off-by: Yannick Bungers <git@innay.de>
2020-07-02 11:22:52 -04:00
logger.info('HedgeDoc has been killed by signal, try to exit gracefully...')
alreadyHandlingTermSignals = true
realtime.maintenance = true
// disconnect all socket.io clients
Object.keys(io.sockets.sockets).forEach(function (key) {
const socket = io.sockets.sockets[key]
// notify client server going into maintenance status
socket.emit('maintenance')
setTimeout(function () {
socket.disconnect(true)
}, 0)
})
if (config.path) {
fs.unlink(config.path, err => {
if (err) {
logger.error(`Could not cleanup socket: ${err.message}`)
} else {
logger.info('Successfully cleaned up socket')
}
})
}
const maxCleanTries = 30
let currentCleanTry = 1
const checkCleanTimer = setInterval(function () {
if (realtime.isReady()) {
models.Revision.checkAllNotesRevision(function (err, notes) {
if (err) {
logger.error('Error while saving note revisions: ' + err)
if (currentCleanTry <= maxCleanTries) {
logger.warn(`Trying again. Try ${currentCleanTry} of ${maxCleanTries}`)
currentCleanTry++
return null
}
logger.error(`Could not save note revisions after ${maxCleanTries} tries! Exiting.`)
process.exit(1)
}
if (!notes || notes.length <= 0) {
clearInterval(checkCleanTimer)
process.exit(0)
}
})
}
}, 200)
}
process.on('SIGINT', handleTermSignals)
process.on('SIGTERM', handleTermSignals)
2017-03-22 03:26:35 -04:00
process.on('SIGQUIT', handleTermSignals)