session-heartbeat.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. /**
  2. * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
  3. * SPDX-License-Identifier: AGPL-3.0-or-later
  4. */
  5. import $ from 'jquery'
  6. import { emit } from '@nextcloud/event-bus'
  7. import { loadState } from '@nextcloud/initial-state'
  8. import { getCurrentUser } from '@nextcloud/auth'
  9. import { generateUrl } from '@nextcloud/router'
  10. import OC from './OC/index.js'
  11. import { setToken as setRequestToken, getToken as getRequestToken } from './OC/requesttoken.js'
  12. let config = null
  13. /**
  14. * The legacy jsunit tests overwrite OC.config before calling initCore
  15. * therefore we need to wait with assigning the config fallback until initCore calls initSessionHeartBeat
  16. */
  17. const loadConfig = () => {
  18. try {
  19. config = loadState('core', 'config')
  20. } catch (e) {
  21. // This fallback is just for our legacy jsunit tests since we have no way to mock loadState calls
  22. config = OC.config
  23. }
  24. }
  25. /**
  26. * session heartbeat (defaults to enabled)
  27. *
  28. * @return {boolean}
  29. */
  30. const keepSessionAlive = () => {
  31. return config.session_keepalive === undefined
  32. || !!config.session_keepalive
  33. }
  34. /**
  35. * get interval in seconds
  36. *
  37. * @return {number}
  38. */
  39. const getInterval = () => {
  40. let interval = NaN
  41. if (config.session_lifetime) {
  42. interval = Math.floor(config.session_lifetime / 2)
  43. }
  44. // minimum one minute, max 24 hours, default 15 minutes
  45. return Math.min(
  46. 24 * 3600,
  47. Math.max(
  48. 60,
  49. isNaN(interval) ? 900 : interval,
  50. ),
  51. )
  52. }
  53. const getToken = async () => {
  54. const url = generateUrl('/csrftoken')
  55. // Not using Axios here as Axios is not stubbable with the sinon fake server
  56. // see https://stackoverflow.com/questions/41516044/sinon-mocha-test-with-async-ajax-calls-didnt-return-promises
  57. // see js/tests/specs/coreSpec.js for the tests
  58. const resp = await $.get(url)
  59. return resp.token
  60. }
  61. const poll = async () => {
  62. try {
  63. const token = await getToken()
  64. setRequestToken(token)
  65. } catch (e) {
  66. console.error('session heartbeat failed', e)
  67. }
  68. }
  69. const startPolling = () => {
  70. const interval = setInterval(poll, getInterval() * 1000)
  71. console.info('session heartbeat polling started')
  72. return interval
  73. }
  74. const registerAutoLogout = () => {
  75. if (!config.auto_logout || !getCurrentUser()) {
  76. return
  77. }
  78. let lastActive = Date.now()
  79. window.addEventListener('mousemove', e => {
  80. lastActive = Date.now()
  81. localStorage.setItem('lastActive', lastActive)
  82. })
  83. window.addEventListener('touchstart', e => {
  84. lastActive = Date.now()
  85. localStorage.setItem('lastActive', lastActive)
  86. })
  87. window.addEventListener('storage', e => {
  88. if (e.key !== 'lastActive') {
  89. return
  90. }
  91. lastActive = e.newValue
  92. })
  93. let intervalId = 0
  94. const logoutCheck = () => {
  95. const timeout = Date.now() - config.session_lifetime * 1000
  96. if (lastActive < timeout) {
  97. clearTimeout(intervalId)
  98. console.info('Inactivity timout reached, logging out')
  99. const logoutUrl = generateUrl('/logout') + '?requesttoken=' + encodeURIComponent(getRequestToken())
  100. window.location = logoutUrl
  101. }
  102. }
  103. intervalId = setInterval(logoutCheck, 1000)
  104. }
  105. /**
  106. * Calls the server periodically to ensure that session and CSRF
  107. * token doesn't expire
  108. */
  109. export const initSessionHeartBeat = () => {
  110. loadConfig()
  111. registerAutoLogout()
  112. if (!keepSessionAlive()) {
  113. console.info('session heartbeat disabled')
  114. return
  115. }
  116. let interval = startPolling()
  117. window.addEventListener('online', async () => {
  118. console.info('browser is online again, resuming heartbeat')
  119. interval = startPolling()
  120. try {
  121. await poll()
  122. console.info('session token successfully updated after resuming network')
  123. // Let apps know we're online and requests will have the new token
  124. emit('networkOnline', {
  125. success: true,
  126. })
  127. } catch (e) {
  128. console.error('could not update session token after resuming network', e)
  129. // Let apps know we're online but requests might have an outdated token
  130. emit('networkOnline', {
  131. success: false,
  132. })
  133. }
  134. })
  135. window.addEventListener('offline', () => {
  136. console.info('browser is offline, stopping heartbeat')
  137. // Let apps know we're offline
  138. emit('networkOffline', {})
  139. clearInterval(interval)
  140. console.info('session heartbeat polling stopped')
  141. })
  142. }