mirror of
https://github.com/status-im/codimd.git
synced 2025-01-12 18:34:30 +00:00
019196484a
Signed-off-by: Raccoon <raccoon@hackmd.io>
344 lines
10 KiB
TypeScript
344 lines
10 KiB
TypeScript
// app
|
|
// external modules
|
|
import * as express from "express";
|
|
import * as ejs from "ejs";
|
|
import * as passport from "passport";
|
|
import * as methodOverride from "method-override";
|
|
import * as cookieParser from "cookie-parser";
|
|
import * as session from "express-session";
|
|
import * as fs from "fs";
|
|
import * as path from "path";
|
|
import * as morgan from "morgan";
|
|
import * as helmet from "helmet";
|
|
import * as i18n from "i18n";
|
|
import * as flash from "connect-flash";
|
|
import {expressMiddleware as expressPrometheusMetricsMiddleware} from "prometheus-api-metrics";
|
|
import * as connectSessionSequelize from 'connect-session-sequelize'
|
|
import * as passportSocketIo from 'passport.socketio'
|
|
import * as SocketIo from 'socket.io'
|
|
import {Server as WsServer} from 'ws'
|
|
// core
|
|
import * as config from "./config";
|
|
import * as logger from "./logger";
|
|
import * as response from "./response";
|
|
import * as models from "./models";
|
|
import * as csp from "./csp";
|
|
import {Environment} from "./config/enum";
|
|
|
|
import {checkVersion, versionCheckMiddleware} from "./web/middleware/checkVersion";
|
|
import TooBusyMiddleware from './middleware/tooBusy'
|
|
import CheckURIValidMiddleware from './middleware/checkURIValid'
|
|
import RedirectWithoutTrailingSlashesMiddleware from './middleware/redirectWithoutTrailingSlashes'
|
|
import CodiMDVersionMiddleware from './middleware/codiMDVersion'
|
|
|
|
import {router as MetricsRouter} from './metrics'
|
|
import {router as MainRouter} from './routes'
|
|
|
|
import * as realtime from './realtime/realtime'
|
|
|
|
const SequelizeStore = connectSessionSequelize(session.Store)
|
|
|
|
function createHttpServer() {
|
|
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: ca,
|
|
dhparam: fs.readFileSync(config.dhParamPath, 'utf8'),
|
|
requestCert: false,
|
|
rejectUnauthorized: false
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
return require('https').createServer(options, app)
|
|
} else {
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
return require('http').createServer(app)
|
|
}
|
|
}
|
|
|
|
// server setup
|
|
const app = express()
|
|
const server = createHttpServer()
|
|
|
|
// API and process monitoring with Prometheus for Node.js micro-service
|
|
app.use(expressPrometheusMetricsMiddleware({
|
|
metricsPath: '/metrics/router',
|
|
excludeRoutes: ['/metrics/codimd']
|
|
}))
|
|
|
|
// logger
|
|
app.use(morgan('combined', {
|
|
stream: logger.stream
|
|
}))
|
|
|
|
// socket io
|
|
|
|
|
|
const io = SocketIo(server)
|
|
io.engine.ws = new WsServer({
|
|
noServer: true,
|
|
perMessageDeflate: false
|
|
})
|
|
|
|
// others
|
|
|
|
// assign socket io to realtime
|
|
realtime.setSocketIo(io)
|
|
|
|
// methodOverride
|
|
app.use(methodOverride('_method'))
|
|
|
|
// session store
|
|
const sessionStore = new SequelizeStore({
|
|
db: models.sequelize
|
|
})
|
|
|
|
// 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')
|
|
}
|
|
|
|
// 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', 'id', 'sr'],
|
|
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, index: false}))
|
|
app.use('/docs', express.static(path.resolve(__dirname, config.docsPath), {maxAge: config.staticCacheTime}))
|
|
app.use('/uploads', express.static(path.resolve(__dirname, config.uploadsPath), {maxAge: config.staticCacheTime}))
|
|
app.use('/default.md', express.static(path.resolve(__dirname, config.defaultNotePath), {maxAge: config.staticCacheTime}))
|
|
app.use(MetricsRouter)
|
|
|
|
// 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
|
|
const tlsSessionStore = {}
|
|
server.on('newSession', function (id: Buffer, data, cb) {
|
|
tlsSessionStore[id.toString('hex')] = data
|
|
cb()
|
|
})
|
|
server.on('resumeSession', function (id: Buffer, cb) {
|
|
cb(null, tlsSessionStore[id.toString('hex')] || null)
|
|
})
|
|
|
|
// middleware which blocks requests when we're too busy
|
|
app.use(TooBusyMiddleware)
|
|
|
|
app.use(flash())
|
|
|
|
// passport
|
|
app.use(passport.initialize())
|
|
app.use(passport.session())
|
|
|
|
// check uri is valid before going further
|
|
app.use(CheckURIValidMiddleware)
|
|
// redirect url without trailing slashes
|
|
app.use(RedirectWithoutTrailingSlashesMiddleware)
|
|
app.use(CodiMDVersionMiddleware)
|
|
|
|
if (config.autoVersionCheck && process.env.NODE_ENV === Environment.production) {
|
|
checkVersion(app)
|
|
app.use(versionCheckMiddleware)
|
|
}
|
|
|
|
// 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.useCDN = config.useCDN
|
|
app.locals.serverURL = config.serverURL
|
|
app.locals.sourceURL = config.sourceURL
|
|
app.locals.privacyPolicyURL = config.privacyPolicyURL
|
|
app.locals.allowAnonymous = config.allowAnonymous
|
|
app.locals.allowAnonymousEdits = config.allowAnonymousEdits
|
|
app.locals.permission = config.permission
|
|
app.locals.allowPDFExport = config.allowPDFExport
|
|
app.locals.authProviders = {
|
|
facebook: config.isFacebookEnable,
|
|
twitter: config.isTwitterEnable,
|
|
github: config.isGitHubEnable,
|
|
bitbucket: config.isBitbucketEnable,
|
|
gitlab: config.isGitLabEnable,
|
|
mattermost: config.isMattermostEnable,
|
|
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
|
|
}
|
|
app.locals.versionInfo = {
|
|
latest: true,
|
|
versionItem: null
|
|
}
|
|
|
|
// Export/Import menu items
|
|
app.locals.enableDropBoxSave = config.isDropboxEnable
|
|
app.locals.enableGitHubGist = config.isGitHubEnable
|
|
app.locals.enableGitlabSnippets = config.isGitlabSnippetsEnable
|
|
|
|
app.use(MainRouter)
|
|
|
|
// response not found if no any route matxches
|
|
app.get('*', function (req, res) {
|
|
response.errorNotFound(req, 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() {
|
|
let address
|
|
const listenCallback = function () {
|
|
const schema = config.useSSL ? 'HTTPS' : 'HTTP'
|
|
logger.info('%s Server listening at %s', schema, address)
|
|
realtime.setMaintenance(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')
|
|
}
|
|
}).catch(err => {
|
|
logger.error('Can\'t sync database')
|
|
logger.error(err.stack)
|
|
logger.error('Process will exit now.')
|
|
process.exit(1)
|
|
})
|
|
|
|
// log uncaught exception
|
|
process.on('uncaughtException', function (err) {
|
|
logger.error('An uncaught exception has occured.')
|
|
logger.error(err)
|
|
console.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.setMaintenance(true)
|
|
realtime.terminate()
|
|
// 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)
|
|
})
|
|
const 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)
|
|
setTimeout(() => {
|
|
process.exit(1)
|
|
}, 5000)
|
|
}
|
|
process.on('SIGINT', handleTermSignals)
|
|
process.on('SIGTERM', handleTermSignals)
|
|
process.on('SIGQUIT', handleTermSignals)
|