WebsocketLoadBalancer.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. const Settings = require('@overleaf/settings')
  2. const logger = require('@overleaf/logger')
  3. const RedisClientManager = require('./RedisClientManager')
  4. const SafeJsonParse = require('./SafeJsonParse')
  5. const EventLogger = require('./EventLogger')
  6. const HealthCheckManager = require('./HealthCheckManager')
  7. const RoomManager = require('./RoomManager')
  8. const ChannelManager = require('./ChannelManager')
  9. const ConnectedUsersManager = require('./ConnectedUsersManager')
  10. const RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST = [
  11. 'otUpdateApplied',
  12. 'otUpdateError',
  13. 'joinDoc',
  14. 'reciveNewDoc',
  15. 'reciveNewFile',
  16. 'reciveNewFolder',
  17. 'reciveEntityMove',
  18. 'reciveEntityRename',
  19. 'removeEntity',
  20. 'accept-changes',
  21. 'projectNameUpdated',
  22. 'rootDocUpdated',
  23. 'toggle-track-changes',
  24. 'projectRenamedOrDeletedByExternalSource',
  25. ]
  26. let WebsocketLoadBalancer
  27. module.exports = WebsocketLoadBalancer = {
  28. rclientPubList: RedisClientManager.createClientList(Settings.redis.pubsub),
  29. rclientSubList: RedisClientManager.createClientList(Settings.redis.pubsub),
  30. shouldDisconnectClient(client, message) {
  31. const userId = client.ol_context.user_id
  32. if (message?.message === 'userRemovedFromProject') {
  33. if (message?.payload?.includes(userId)) {
  34. return true
  35. }
  36. } else if (message?.message === 'project:publicAccessLevel:changed') {
  37. const [info] = message.payload
  38. if (
  39. info.newAccessLevel === 'private' &&
  40. !client.ol_context.is_invited_member
  41. ) {
  42. return true
  43. }
  44. }
  45. return false
  46. },
  47. emitToRoom(roomId, message, ...payload) {
  48. if (!roomId) {
  49. logger.warn(
  50. { message, payload },
  51. 'no room_id provided, ignoring emitToRoom'
  52. )
  53. return
  54. }
  55. const data = JSON.stringify({
  56. room_id: roomId,
  57. message,
  58. payload,
  59. })
  60. logger.debug(
  61. { roomId, message, payload, length: data.length },
  62. 'emitting to room'
  63. )
  64. this.rclientPubList.map(rclientPub =>
  65. ChannelManager.publish(rclientPub, 'editor-events', roomId, data)
  66. )
  67. },
  68. emitToAll(message, ...payload) {
  69. this.emitToRoom('all', message, ...payload)
  70. },
  71. listenForEditorEvents(io) {
  72. logger.debug(
  73. { rclients: this.rclientSubList.length },
  74. 'listening for editor events'
  75. )
  76. for (const rclientSub of this.rclientSubList) {
  77. rclientSub.subscribe('editor-events')
  78. rclientSub.on('message', function (channel, message) {
  79. if (Settings.debugEvents > 0) {
  80. EventLogger.debugEvent(channel, message)
  81. }
  82. WebsocketLoadBalancer._processEditorEvent(io, channel, message)
  83. })
  84. }
  85. this.handleRoomUpdates(this.rclientSubList)
  86. },
  87. handleRoomUpdates(rclientSubList) {
  88. const roomEvents = RoomManager.eventSource()
  89. roomEvents.on('project-active', function (projectId) {
  90. const subscribePromises = rclientSubList.map(rclient =>
  91. ChannelManager.subscribe(rclient, 'editor-events', projectId)
  92. )
  93. RoomManager.emitOnCompletion(
  94. subscribePromises,
  95. `project-subscribed-${projectId}`
  96. )
  97. })
  98. roomEvents.on('project-empty', projectId =>
  99. rclientSubList.map(rclient =>
  100. ChannelManager.unsubscribe(rclient, 'editor-events', projectId)
  101. )
  102. )
  103. },
  104. _processEditorEvent(io, channel, message) {
  105. SafeJsonParse.parse(message, function (error, message) {
  106. if (error) {
  107. logger.error({ err: error, channel }, 'error parsing JSON')
  108. return
  109. }
  110. if (message.room_id === 'all') {
  111. io.sockets.emit(message.message, ...message.payload)
  112. } else if (
  113. message.message === 'clientTracking.refresh' &&
  114. message.room_id
  115. ) {
  116. const clientList = io.sockets.clients(message.room_id)
  117. logger.debug(
  118. {
  119. channel,
  120. message: message.message,
  121. roomId: message.room_id,
  122. messageId: message._id,
  123. socketIoClients: clientList.map(client => client.id),
  124. },
  125. 'refreshing client list'
  126. )
  127. for (const client of clientList) {
  128. ConnectedUsersManager.refreshClient(message.room_id, client.publicId)
  129. }
  130. } else if (message.room_id) {
  131. if (message._id && Settings.checkEventOrder) {
  132. const status = EventLogger.checkEventOrder(
  133. 'editor-events',
  134. message._id,
  135. message
  136. )
  137. if (status === 'duplicate') {
  138. return // skip duplicate events
  139. }
  140. }
  141. const isRestrictedMessage =
  142. !RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST.includes(message.message)
  143. // send messages only to unique clients (due to duplicate entries in io.sockets.clients)
  144. const clientList = io.sockets.clients(message.room_id)
  145. // avoid unnecessary work if no clients are connected
  146. if (clientList.length === 0) {
  147. return
  148. }
  149. logger.debug(
  150. {
  151. channel,
  152. message: message.message,
  153. roomId: message.room_id,
  154. messageId: message._id,
  155. socketIoClients: clientList.map(client => client.id),
  156. },
  157. 'distributing event to clients'
  158. )
  159. const seen = new Map()
  160. for (const client of clientList) {
  161. if (!seen.has(client.id)) {
  162. seen.set(client.id, true)
  163. if (WebsocketLoadBalancer.shouldDisconnectClient(client, message)) {
  164. logger.debug(
  165. {
  166. message,
  167. userId: client?.ol_context?.user_id,
  168. projectId: client?.ol_context?.project_id,
  169. },
  170. 'disconnecting client'
  171. )
  172. client.emit('project:access:revoked')
  173. client.disconnect()
  174. } else {
  175. if (isRestrictedMessage && client.ol_context.is_restricted_user) {
  176. // hide restricted message
  177. logger.debug(
  178. {
  179. message,
  180. clientId: client.id,
  181. userId: client.ol_context.user_id,
  182. projectId: client.ol_context.project_id,
  183. },
  184. 'hiding restricted message from client'
  185. )
  186. } else {
  187. client.emit(message.message, ...message.payload)
  188. }
  189. }
  190. }
  191. }
  192. } else if (message.health_check) {
  193. logger.debug(
  194. { message },
  195. 'got health check message in editor events channel'
  196. )
  197. HealthCheckManager.check(channel, message.key)
  198. }
  199. })
  200. },
  201. }