RealTimeClient.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import io from 'socket.io-client'
  2. import Settings from '@overleaf/settings'
  3. import redis from '@overleaf/redis-wrapper'
  4. import uidSafe from 'uid-safe'
  5. import signature from 'cookie-signature'
  6. import { callbackify } from 'node:util'
  7. import { fetchJson, fetchNothing } from '@overleaf/fetch-utils'
  8. import { XMLHttpRequest } from '../../libs/XMLHttpRequest.js'
  9. const rclient = redis.createClient(Settings.redis.websessions)
  10. const uid = uidSafe.sync
  11. io.util.request = function () {
  12. const xhr = new XMLHttpRequest()
  13. const _open = xhr.open
  14. xhr.open = function () {
  15. _open.apply(xhr, arguments)
  16. if (Client.cookie != null) {
  17. return xhr.setRequestHeader('Cookie', Client.cookie)
  18. }
  19. }
  20. return xhr
  21. }
  22. async function setSession(session) {
  23. const sessionId = uid(24)
  24. session.cookie = {}
  25. await rclient.set('sess:' + sessionId, JSON.stringify(session))
  26. Client.cookieSignedWith = {}
  27. // prepare cookie strings for all supported session secrets
  28. for (const secretName of [
  29. 'sessionSecret',
  30. 'sessionSecretFallback',
  31. 'sessionSecretUpcoming',
  32. ]) {
  33. const secret = Settings.security[secretName]
  34. const cookieKey = 's:' + signature.sign(sessionId, secret)
  35. Client.cookieSignedWith[secretName] = `${Settings.cookieName}=${cookieKey}`
  36. }
  37. // default to the current session secret
  38. Client.cookie = Client.cookieSignedWith.sessionSecret
  39. }
  40. async function setAnonSession(projectId, anonymousAccessToken) {
  41. await Client.promises.setSession({
  42. anonTokenAccess: {
  43. [projectId]: anonymousAccessToken,
  44. },
  45. })
  46. }
  47. function connect(projectId) {
  48. const client = io.connect('http://127.0.0.1:3026', {
  49. 'force new connection': true,
  50. query: new URLSearchParams({ projectId }).toString(),
  51. })
  52. let disconnected = false
  53. client.on('disconnect', () => {
  54. disconnected = true
  55. })
  56. const promise = new Promise((resolve, reject) => {
  57. client.on('connectionRejected', err => {
  58. // Wait for disconnect ahead of continuing with the test sequence.
  59. setTimeout(() => {
  60. if (!disconnected) {
  61. throw new Error('should disconnect after connectionRejected')
  62. }
  63. reject(err)
  64. }, 10)
  65. })
  66. client.on('joinProjectResponse', resp => {
  67. const { publicId, project, permissionsLevel, protocolVersion } = resp
  68. client.publicId = publicId
  69. resolve({ project, permissionsLevel, protocolVersion, client })
  70. })
  71. })
  72. return { client, promise }
  73. }
  74. async function getConnectedClients() {
  75. return await fetchJson('http://127.0.0.1:3026/clients')
  76. }
  77. async function countConnectedClients(projectId) {
  78. return await fetchJson(
  79. `http://127.0.0.1:3026/project/${projectId}/count-connected-clients`
  80. )
  81. }
  82. async function getConnectedClient(clientId) {
  83. try {
  84. return await fetchJson(`http://127.0.0.1:3026/clients/${clientId}`)
  85. } catch (err) {
  86. if (err.info?.status === 404) throw new Error('not found')
  87. throw err
  88. }
  89. }
  90. async function disconnectClient(clientId) {
  91. await fetchNothing(`http://127.0.0.1:3026/client/${clientId}/disconnect`, {
  92. method: 'POST',
  93. })
  94. }
  95. async function disconnectAllClients() {
  96. const clients = await Client.promises.getConnectedClients()
  97. await Promise.all(
  98. clients.map(clientView =>
  99. Client.promises.disconnectClient(clientView.client_id)
  100. )
  101. )
  102. }
  103. const Client = {
  104. cookie: null,
  105. setSession: callbackify(setSession),
  106. setAnonSession: callbackify(setAnonSession),
  107. connect: (projectId, callback) => {
  108. const { client, promise } = connect(projectId)
  109. if (callback) {
  110. promise
  111. .then(({ project, permissionsLevel, protocolVersion }) =>
  112. callback(null, project, permissionsLevel, protocolVersion)
  113. )
  114. .catch(err => callback(err))
  115. }
  116. return client
  117. },
  118. getConnectedClients: callbackify(getConnectedClients),
  119. countConnectedClients: callbackify(countConnectedClients),
  120. getConnectedClient: callbackify(getConnectedClient),
  121. disconnectClient: callbackify(disconnectClient),
  122. disconnectAllClients: callbackify(disconnectAllClients),
  123. promises: {
  124. setSession,
  125. setAnonSession,
  126. connect: async projectId => {
  127. const { client, promise } = connect(projectId)
  128. const { project, permissionsLevel, protocolVersion } = await promise
  129. return { project, permissionsLevel, protocolVersion, client }
  130. },
  131. getConnectedClients,
  132. countConnectedClients,
  133. getConnectedClient,
  134. disconnectClient,
  135. disconnectAllClients,
  136. },
  137. }
  138. export default Client