WebsocketLoadBalancer.js 6.5 KB

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