app.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. // Metrics must be initialized before importing anything else
  2. require('@overleaf/metrics/initialize')
  3. const Metrics = require('@overleaf/metrics')
  4. const Settings = require('@overleaf/settings')
  5. const async = require('async')
  6. const logger = require('@overleaf/logger')
  7. logger.initialize('real-time')
  8. Metrics.event_loop.monitor(logger)
  9. Metrics.open_sockets.monitor()
  10. const express = require('express')
  11. const session = require('express-session')
  12. const redis = require('@overleaf/redis-wrapper')
  13. const sessionRedisClient = redis.createClient(Settings.redis.websessions)
  14. const RedisStore = require('connect-redis')(session)
  15. const SessionSockets = require('./app/js/SessionSockets')
  16. const CookieParser = require('cookie-parser')
  17. const DrainManager = require('./app/js/DrainManager')
  18. const HealthCheckManager = require('./app/js/HealthCheckManager')
  19. const DeploymentManager = require('./app/js/DeploymentManager')
  20. const Path = require('node:path')
  21. // NOTE: debug is invoked for every blob that is put on the wire
  22. const socketIoLogger = {
  23. error(...message) {
  24. logger.debug({ fromSocketIo: true, originalLevel: 'error' }, ...message)
  25. },
  26. warn(...message) {
  27. logger.debug({ fromSocketIo: true, originalLevel: 'warn' }, ...message)
  28. },
  29. info() {},
  30. debug() {},
  31. log() {},
  32. }
  33. // monitor status file to take dark deployments out of the load-balancer
  34. DeploymentManager.initialise()
  35. // Set up socket.io server
  36. const app = express()
  37. const server = require('node:http').createServer(app)
  38. server.keepAliveTimeout = Settings.keepAliveTimeoutMs
  39. const io = require('socket.io').listen(server, {
  40. logger: socketIoLogger,
  41. })
  42. // Bind to sessions
  43. const sessionStore = new RedisStore({ client: sessionRedisClient })
  44. if (!Settings.security.sessionSecret) {
  45. throw new Error('No SESSION_SECRET provided.')
  46. }
  47. const sessionSecrets = [
  48. Settings.security.sessionSecret,
  49. Settings.security.sessionSecretUpcoming,
  50. Settings.security.sessionSecretFallback,
  51. ].filter(Boolean)
  52. const cookieParser = CookieParser(sessionSecrets)
  53. const sessionSockets = new SessionSockets(
  54. io,
  55. sessionStore,
  56. cookieParser,
  57. Settings.cookieName
  58. )
  59. Metrics.injectMetricsRoute(app)
  60. io.configure(function () {
  61. // Don't use socket.io to serve client
  62. io.disable('browser client')
  63. // Fix for Safari 5 error of "Error during WebSocket handshake: location mismatch"
  64. // See http://answers.dotcloud.com/question/578/problem-with-websocket-over-ssl-in-safari-with
  65. io.set('match origin protocol', true)
  66. io.set('transports', ['websocket', 'xhr-polling'])
  67. if (Settings.allowedCorsOrigins) {
  68. // Create a regex for matching origins, allowing wildcard subdomains
  69. const allowedCorsOriginsRegex = new RegExp(
  70. `^${Settings.allowedCorsOrigins.replaceAll('.', '\\.').replace('://*', '://[^.]+')}(?::443)?$`
  71. )
  72. io.set('origins', function (origin, req) {
  73. if (!origin) {
  74. // There is no origin or referer header - this is likely a same-site request.
  75. logger.warn({ req }, 'No origin or referer header')
  76. return true
  77. }
  78. const normalizedOrigin = URL.parse(origin).origin
  79. const originIsValid = allowedCorsOriginsRegex.test(normalizedOrigin)
  80. if (req.headers.origin) {
  81. if (!originIsValid) {
  82. logger.warn(
  83. { normalizedOrigin, origin, req },
  84. 'Origin header does not match allowed origins'
  85. )
  86. }
  87. return originIsValid
  88. }
  89. if (!originIsValid) {
  90. // There is no Origin header and the Referrer does not satisfy the
  91. // constraints. We're going to pass this anyway for now but log it
  92. logger.warn(
  93. { req, referer: req.headers.referer },
  94. 'Referrer header does not match allowed origins'
  95. )
  96. }
  97. return true
  98. })
  99. }
  100. })
  101. // Serve socket.io.js client file from imported dist folder
  102. // The express sendFile method correctly handles conditional
  103. // requests using the last-modified time and etag (which is
  104. // a combination of mtime and size)
  105. const socketIOClientFolder = require('socket.io-client').dist
  106. app.get('/socket.io/socket.io.js', function (req, res) {
  107. res.sendFile(Path.join(socketIOClientFolder, 'socket.io.min.js'))
  108. })
  109. // a 200 response on '/' is required for load balancer health checks
  110. // these operate separately from kubernetes readiness checks
  111. app.get('/', function (req, res) {
  112. if (Settings.shutDownInProgress || DeploymentManager.deploymentIsClosed()) {
  113. res.sendStatus(503) // Service unavailable
  114. } else {
  115. res.send('real-time is open')
  116. }
  117. })
  118. app.get('/status', function (req, res) {
  119. if (Settings.shutDownInProgress) {
  120. res.sendStatus(503) // Service unavailable
  121. } else {
  122. res.send('real-time is alive')
  123. }
  124. })
  125. app.get('/debug/events', function (req, res) {
  126. Settings.debugEvents = parseInt(req.query.count, 10) || 20
  127. logger.info({ count: Settings.debugEvents }, 'starting debug mode')
  128. res.send(`debug mode will log next ${Settings.debugEvents} events`)
  129. })
  130. const rclient = require('@overleaf/redis-wrapper').createClient(
  131. Settings.redis.realtime
  132. )
  133. function healthCheck(req, res) {
  134. rclient.healthCheck(function (error) {
  135. if (error) {
  136. logger.err({ err: error }, 'failed redis health check')
  137. res.sendStatus(500)
  138. } else if (HealthCheckManager.isFailing()) {
  139. const status = HealthCheckManager.status()
  140. logger.err({ pubSubErrors: status }, 'failed pubsub health check')
  141. res.sendStatus(500)
  142. } else {
  143. res.sendStatus(200)
  144. }
  145. })
  146. }
  147. app.get(
  148. '/health_check',
  149. (req, res, next) => {
  150. if (Settings.shutDownComplete) {
  151. return res.sendStatus(503)
  152. }
  153. next()
  154. },
  155. healthCheck
  156. )
  157. app.get('/health_check/redis', healthCheck)
  158. // log http requests for routes defined from this point onwards
  159. app.use(Metrics.http.monitor(logger))
  160. const Router = require('./app/js/Router')
  161. Router.configure(app, io, sessionSockets)
  162. const WebsocketLoadBalancer = require('./app/js/WebsocketLoadBalancer')
  163. WebsocketLoadBalancer.listenForEditorEvents(io)
  164. const DocumentUpdaterController = require('./app/js/DocumentUpdaterController')
  165. DocumentUpdaterController.listenForUpdatesFromDocumentUpdater(io)
  166. const { port } = Settings.internal.realTime
  167. const { host } = Settings.internal.realTime
  168. server.listen(port, host, function (error) {
  169. if (error) {
  170. throw error
  171. }
  172. logger.info(`realtime starting up, listening on ${host}:${port}`)
  173. })
  174. // Stop huge stack traces in logs from all the socket.io parsing steps.
  175. Error.stackTraceLimit = 10
  176. function shutdownAfterAllClientsHaveDisconnected() {
  177. const connectedClients = io.sockets.clients().length
  178. if (connectedClients === 0) {
  179. logger.info({}, 'no clients connected, exiting')
  180. process.exit()
  181. } else {
  182. logger.info(
  183. { connectedClients },
  184. 'clients still connected, not shutting down yet'
  185. )
  186. setTimeout(() => shutdownAfterAllClientsHaveDisconnected(), 5_000)
  187. }
  188. }
  189. function drainAndShutdown(signal) {
  190. if (Settings.shutDownInProgress) {
  191. logger.info({ signal }, 'shutdown already in progress, ignoring signal')
  192. } else {
  193. Settings.shutDownInProgress = true
  194. const { statusCheckInterval } = Settings
  195. if (statusCheckInterval) {
  196. logger.info(
  197. { signal },
  198. `received interrupt, delay drain by ${statusCheckInterval}ms`
  199. )
  200. }
  201. setTimeout(function () {
  202. logger.info(
  203. { signal },
  204. `received interrupt, starting drain over ${shutdownDrainTimeWindow} mins`
  205. )
  206. DrainManager.startDrainTimeWindow(io, shutdownDrainTimeWindow, () => {
  207. shutdownAfterAllClientsHaveDisconnected()
  208. setTimeout(() => {
  209. const staleClients = io.sockets.clients()
  210. if (staleClients.length !== 0) {
  211. logger.info(
  212. { staleClients: staleClients.map(client => client.id) },
  213. 'forcefully disconnecting stale clients'
  214. )
  215. staleClients.forEach(client => {
  216. client.disconnect()
  217. })
  218. }
  219. // Mark the node as unhealthy.
  220. Settings.shutDownComplete = true
  221. }, Settings.gracefulReconnectTimeoutMs)
  222. })
  223. }, statusCheckInterval)
  224. }
  225. }
  226. Settings.shutDownInProgress = false
  227. Settings.shutDownScheduled = false
  228. const shutdownDrainTimeWindow = parseInt(Settings.shutdownDrainTimeWindow, 10)
  229. if (Settings.shutdownDrainTimeWindow) {
  230. logger.info({ shutdownDrainTimeWindow }, 'shutdownDrainTimeWindow enabled')
  231. for (const signal of [
  232. 'SIGINT',
  233. 'SIGHUP',
  234. 'SIGQUIT',
  235. 'SIGUSR1',
  236. 'SIGUSR2',
  237. 'SIGTERM',
  238. 'SIGABRT',
  239. ]) {
  240. process.on(signal, drainAndShutdown)
  241. } // signal is passed as argument to event handler
  242. // global exception handler
  243. if (Settings.errors && Settings.errors.catchUncaughtErrors) {
  244. process.removeAllListeners('uncaughtException')
  245. process.on('uncaughtException', function (error) {
  246. if (
  247. [
  248. 'ETIMEDOUT',
  249. 'EHOSTUNREACH',
  250. 'EPIPE',
  251. 'ECONNRESET',
  252. 'ERR_STREAM_WRITE_AFTER_END',
  253. ].includes(error.code) ||
  254. // socket.io error handler sending on polling connection again.
  255. (error.code === 'ERR_HTTP_HEADERS_SENT' &&
  256. error.stack &&
  257. error.stack.includes('Transport.error'))
  258. ) {
  259. Metrics.inc('disconnected_write', 1, { status: error.code })
  260. return logger.warn(
  261. { err: error },
  262. 'attempted to write to disconnected client'
  263. )
  264. }
  265. logger.error({ err: error }, 'uncaught exception')
  266. if (
  267. Settings.errors?.shutdownOnUncaughtError &&
  268. !Settings.shutDownScheduled
  269. ) {
  270. Settings.shutDownScheduled = true
  271. const delay = Math.ceil(
  272. Math.random() * 60 * Math.max(io.sockets.clients().length, 1_000)
  273. )
  274. logger.info({ delay }, 'delaying shutdown on uncaught error')
  275. setTimeout(() => drainAndShutdown('SIGABRT'), delay)
  276. }
  277. })
  278. }
  279. }
  280. if (Settings.continualPubsubTraffic) {
  281. logger.debug('continualPubsubTraffic enabled')
  282. const pubsubClient = redis.createClient(Settings.redis.pubsub)
  283. const clusterClient = redis.createClient(Settings.redis.websessions)
  284. const publishJob = function (channel, callback) {
  285. const checker = new HealthCheckManager(channel)
  286. logger.debug({ channel }, 'sending pub to keep connection alive')
  287. const json = JSON.stringify({
  288. health_check: true,
  289. key: checker.id,
  290. date: new Date().toString(),
  291. })
  292. Metrics.summary(`redis.publish.${channel}`, json.length)
  293. pubsubClient.publish(channel, json, function (err) {
  294. if (err) {
  295. logger.err({ err, channel }, 'error publishing pubsub traffic to redis')
  296. }
  297. const blob = JSON.stringify({ keep: 'alive' })
  298. Metrics.summary('redis.publish.cluster-continual-traffic', blob.length)
  299. clusterClient.publish('cluster-continual-traffic', blob, callback)
  300. })
  301. }
  302. const runPubSubTraffic = () =>
  303. async.map(['applied-ops', 'editor-events'], publishJob, () =>
  304. setTimeout(runPubSubTraffic, 1000 * 20)
  305. )
  306. runPubSubTraffic()
  307. }