WebsocketLoadBalancer.js 6.9 KB

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