Server.mjs 12 KB

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