Server.js 12 KB

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