WebsocketLoadBalancer.js 6.8 KB

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