Server.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. const Path = require('path')
  2. const express = require('express')
  3. const Settings = require('@overleaf/settings')
  4. const logger = require('@overleaf/logger')
  5. const metrics = require('@overleaf/metrics')
  6. const Validation = require('./Validation')
  7. const csp = require('./CSP')
  8. const Router = require('../router')
  9. const helmet = require('helmet')
  10. const UserSessionsRedis = require('../Features/User/UserSessionsRedis')
  11. const Csrf = require('./Csrf')
  12. const sessionsRedisClient = UserSessionsRedis.client()
  13. const SessionAutostartMiddleware = require('./SessionAutostartMiddleware')
  14. const SessionStoreManager = require('./SessionStoreManager')
  15. const AnalyticsManager = require('../Features/Analytics/AnalyticsManager')
  16. const session = require('express-session')
  17. const RedisStore = require('connect-redis')(session)
  18. const bodyParser = require('./BodyParserWrapper')
  19. const methodOverride = require('method-override')
  20. const cookieParser = require('cookie-parser')
  21. const bearerTokenMiddleware = require('express-bearer-token')
  22. const passport = require('passport')
  23. const LocalStrategy = require('passport-local').Strategy
  24. const oneDayInMilliseconds = 86400000
  25. const ReferalConnect = require('../Features/Referal/ReferalConnect')
  26. const RedirectManager = require('./RedirectManager')
  27. const translations = require('./Translations')
  28. const Views = require('./Views')
  29. const Features = require('./Features')
  30. const ErrorController = require('../Features/Errors/ErrorController')
  31. const HttpErrorHandler = require('../Features/Errors/HttpErrorHandler')
  32. const UserSessionsManager = require('../Features/User/UserSessionsManager')
  33. const AuthenticationController = require('../Features/Authentication/AuthenticationController')
  34. const SessionManager = require('../Features/Authentication/SessionManager')
  35. const {
  36. hasAdminAccess,
  37. } = require('../Features/Helpers/AdminAuthorizationHelper')
  38. const Modules = require('./Modules')
  39. const expressLocals = require('./ExpressLocals')
  40. const STATIC_CACHE_AGE = Settings.cacheStaticAssets
  41. ? oneDayInMilliseconds * 365
  42. : 0
  43. // Init the session store
  44. const sessionStore = new RedisStore({ client: sessionsRedisClient })
  45. const app = express()
  46. const webRouter = express.Router()
  47. const privateApiRouter = express.Router()
  48. const publicApiRouter = express.Router()
  49. if (Settings.behindProxy) {
  50. app.set('trust proxy', Settings.trustedProxyIps || true)
  51. /**
  52. * Handle the X-Original-Forwarded-For header.
  53. *
  54. * The nginx ingress sends us the contents of X-Forwarded-For it received in
  55. * X-Original-Forwarded-For. Express expects all proxy IPs to be in a comma
  56. * separated list in X-Forwarded-For.
  57. */
  58. app.use(function getForwardedForHeader(req, res, next) {
  59. if (
  60. req.headers['x-original-forwarded-for'] &&
  61. req.headers['x-forwarded-for']
  62. ) {
  63. req.headers['x-forwarded-for'] =
  64. req.headers['x-original-forwarded-for'] +
  65. ', ' +
  66. req.headers['x-forwarded-for']
  67. }
  68. next()
  69. })
  70. }
  71. // `req.ip` is a getter on the underlying socket.
  72. // The socket details are freed as the connection is dropped -- aka aborted.
  73. // Hence `req.ip` may read `undefined` upon connection drop.
  74. // A couple of places require a valid IP at all times. Cache it!
  75. const ORIGINAL_REQ_IP = Object.getOwnPropertyDescriptor(
  76. Object.getPrototypeOf(app.request),
  77. 'ip'
  78. ).get
  79. Object.defineProperty(app.request, 'ip', {
  80. configurable: true,
  81. enumerable: true,
  82. get: function ipWithCache() {
  83. const ip = ORIGINAL_REQ_IP.call(this)
  84. // Shadow the prototype level getter with a property on the instance.
  85. // Any future access on `req.ip` will get served by the instance property.
  86. Object.defineProperty(this, 'ip', { value: ip })
  87. return ip
  88. },
  89. })
  90. app.use(function ignoreAbortedConnections(req, res, next) {
  91. if (req.destroyed) {
  92. // Request has been aborted already.
  93. return
  94. }
  95. // Implicitly cache the ip, see above.
  96. if (!req.ip) {
  97. // Critical connection details are missing.
  98. return
  99. }
  100. next()
  101. })
  102. if (Settings.exposeHostname) {
  103. const HOSTNAME = require('os').hostname()
  104. app.use(function exposeHostname(req, res, next) {
  105. res.setHeader('X-Served-By', HOSTNAME)
  106. next()
  107. })
  108. }
  109. webRouter.use(
  110. express.static(Path.join(__dirname, '/../../../public'), {
  111. maxAge: STATIC_CACHE_AGE,
  112. setHeaders: csp.removeCSPHeaders,
  113. })
  114. )
  115. app.set('views', Path.join(__dirname, '/../../views'))
  116. app.set('view engine', 'pug')
  117. Modules.loadViewIncludes(app)
  118. Modules.registerAppMiddleware(app)
  119. app.use(bodyParser.urlencoded({ extended: true, limit: '2mb' }))
  120. app.use(bodyParser.json({ limit: Settings.max_json_request_size }))
  121. app.use(methodOverride())
  122. // add explicit name for telemetry
  123. app.use(bearerTokenMiddleware())
  124. app.use(metrics.http.monitor(logger))
  125. if (Settings.blockCrossOriginRequests) {
  126. app.use(Csrf.blockCrossOriginRequests())
  127. }
  128. RedirectManager.apply(webRouter)
  129. webRouter.use(cookieParser(Settings.security.sessionSecret))
  130. SessionAutostartMiddleware.applyInitialMiddleware(webRouter)
  131. webRouter.use(
  132. session({
  133. resave: false,
  134. saveUninitialized: false,
  135. secret: Settings.security.sessionSecret,
  136. proxy: Settings.behindProxy,
  137. cookie: {
  138. domain: Settings.cookieDomain,
  139. maxAge: Settings.cookieSessionLength, // in milliseconds, see https://github.com/expressjs/session#cookiemaxage
  140. secure: Settings.secureCookie,
  141. sameSite: Settings.sameSiteCookie,
  142. },
  143. store: sessionStore,
  144. key: Settings.cookieName,
  145. rolling: Settings.cookieRollingSession === true,
  146. })
  147. )
  148. if (Features.hasFeature('saas')) {
  149. webRouter.use(AnalyticsManager.analyticsIdMiddleware)
  150. }
  151. // patch the session store to generate a validation token for every new session
  152. SessionStoreManager.enableValidationToken(sessionStore)
  153. // use middleware to reject all requests with invalid tokens
  154. webRouter.use(SessionStoreManager.validationMiddleware)
  155. // passport
  156. webRouter.use(passport.initialize())
  157. webRouter.use(passport.session())
  158. passport.use(
  159. new LocalStrategy(
  160. {
  161. passReqToCallback: true,
  162. usernameField: 'email',
  163. passwordField: 'password',
  164. },
  165. AuthenticationController.doPassportLogin
  166. )
  167. )
  168. passport.serializeUser(AuthenticationController.serializeUser)
  169. passport.deserializeUser(AuthenticationController.deserializeUser)
  170. Modules.hooks.fire('passportSetup', passport, function (err) {
  171. if (err != null) {
  172. logger.err({ err }, 'error setting up passport in modules')
  173. }
  174. })
  175. Modules.applyNonCsrfRouter(webRouter, privateApiRouter, publicApiRouter)
  176. webRouter.csrf = new Csrf()
  177. webRouter.use(webRouter.csrf.middleware)
  178. webRouter.use(translations.i18nMiddleware)
  179. webRouter.use(translations.setLangBasedOnDomainMiddleware)
  180. if (Settings.cookieRollingSession) {
  181. // Measure expiry from last request, not last login
  182. webRouter.use(function touchSession(req, res, next) {
  183. if (!req.session.noSessionCallback) {
  184. req.session.touch()
  185. if (SessionManager.isUserLoggedIn(req.session)) {
  186. UserSessionsManager.touch(
  187. SessionManager.getSessionUser(req.session),
  188. err => {
  189. if (err) {
  190. logger.err({ err }, 'error extending user session')
  191. }
  192. }
  193. )
  194. }
  195. }
  196. next()
  197. })
  198. }
  199. webRouter.use(ReferalConnect.use)
  200. expressLocals(webRouter, privateApiRouter, publicApiRouter)
  201. webRouter.use(SessionAutostartMiddleware.invokeCallbackMiddleware)
  202. webRouter.use(function checkIfSiteClosed(req, res, next) {
  203. if (Settings.siteIsOpen) {
  204. next()
  205. } else if (hasAdminAccess(SessionManager.getSessionUser(req.session))) {
  206. next()
  207. } else {
  208. HttpErrorHandler.maintenance(req, res)
  209. }
  210. })
  211. webRouter.use(function checkIfEditorClosed(req, res, next) {
  212. if (Settings.editorIsOpen) {
  213. next()
  214. } else if (req.url.indexOf('/admin') === 0) {
  215. next()
  216. } else {
  217. HttpErrorHandler.maintenance(req, res)
  218. }
  219. })
  220. webRouter.use(AuthenticationController.validateAdmin)
  221. // add security headers using Helmet
  222. const noCacheMiddleware = require('nocache')()
  223. webRouter.use(function addNoCacheHeader(req, res, next) {
  224. const isProjectPage = /^\/project\/[a-f0-9]{24}$/.test(req.path)
  225. if (isProjectPage) {
  226. // always set no-cache headers on a project page, as it could be an anonymous token viewer
  227. return noCacheMiddleware(req, res, next)
  228. }
  229. const isProjectFile = /^\/project\/[a-f0-9]{24}\/file\/[a-f0-9]{24}$/.test(
  230. req.path
  231. )
  232. if (isProjectFile) {
  233. // don't set no-cache headers on a project file, as it's immutable and can be cached (privately)
  234. return next()
  235. }
  236. const isLoggedIn = SessionManager.isUserLoggedIn(req.session)
  237. if (isLoggedIn) {
  238. // always set no-cache headers for authenticated users (apart from project files, above)
  239. return noCacheMiddleware(req, res, next)
  240. }
  241. // allow other responses (anonymous users, except for project pages) to be cached
  242. return next()
  243. })
  244. webRouter.use(
  245. helmet({
  246. // note that more headers are added by default
  247. dnsPrefetchControl: false,
  248. referrerPolicy: { policy: 'origin-when-cross-origin' },
  249. hsts: false,
  250. // Disabled because it's impractical to include every resource via CORS or
  251. // with the magic CORP header
  252. crossOriginEmbedderPolicy: false,
  253. // We need to be able to share the context of some popups. For example,
  254. // when Recurly opens Paypal in a popup.
  255. crossOriginOpenerPolicy: { policy: 'same-origin-allow-popups' },
  256. // Disabled because it's not a security header and has possibly-unwanted
  257. // effects
  258. originAgentCluster: false,
  259. // We have custom handling for CSP below, so Helmet's default is disabled
  260. contentSecurityPolicy: false,
  261. })
  262. )
  263. // add CSP header to HTML-rendering routes, if enabled
  264. if (Settings.csp && Settings.csp.enabled) {
  265. logger.debug('adding CSP header to rendered routes', Settings.csp)
  266. app.use(csp(Settings.csp))
  267. }
  268. logger.debug('creating HTTP server'.yellow)
  269. const server = require('http').createServer(app)
  270. // provide settings for separate web and api processes
  271. if (Settings.enabledServices.includes('api')) {
  272. logger.debug('providing api router')
  273. app.use(privateApiRouter)
  274. app.use(Validation.errorMiddleware)
  275. app.use(ErrorController.handleApiError)
  276. }
  277. if (Settings.enabledServices.includes('web')) {
  278. logger.debug('providing web router')
  279. if (Settings.precompilePugTemplatesAtBootTime) {
  280. logger.debug('precompiling views for web in production environment')
  281. Views.precompileViews(app)
  282. }
  283. if (app.get('env') === 'test') {
  284. logger.debug('enabling view cache for acceptance tests')
  285. app.enable('view cache')
  286. }
  287. app.use(publicApiRouter) // public API goes with web router for public access
  288. app.use(Validation.errorMiddleware)
  289. app.use(ErrorController.handleApiError)
  290. app.use(webRouter)
  291. app.use(Validation.errorMiddleware)
  292. app.use(ErrorController.handleError)
  293. }
  294. metrics.injectMetricsRoute(webRouter)
  295. metrics.injectMetricsRoute(privateApiRouter)
  296. Router.initialize(webRouter, privateApiRouter, publicApiRouter)
  297. module.exports = {
  298. app,
  299. server,
  300. }