You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

session-heartbeat.js 4.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. /**
  2. * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
  3. *
  4. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  5. * @author John Molakvoæ <skjnldsv@protonmail.com>
  6. * @author Julius Härtl <jus@bitgrid.net>
  7. * @author Roeland Jago Douma <roeland@famdouma.nl>
  8. *
  9. * @license AGPL-3.0-or-later
  10. *
  11. * This program is free software: you can redistribute it and/or modify
  12. * it under the terms of the GNU Affero General Public License as
  13. * published by the Free Software Foundation, either version 3 of the
  14. * License, or (at your option) any later version.
  15. *
  16. * This program is distributed in the hope that it will be useful,
  17. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. * GNU Affero General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU Affero General Public License
  22. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  23. *
  24. */
  25. import $ from 'jquery'
  26. import { emit } from '@nextcloud/event-bus'
  27. import { loadState } from '@nextcloud/initial-state'
  28. import { getCurrentUser } from '@nextcloud/auth'
  29. import { generateUrl } from '@nextcloud/router'
  30. import OC from './OC/index.js'
  31. import { setToken as setRequestToken, getToken as getRequestToken } from './OC/requesttoken.js'
  32. let config = null
  33. /**
  34. * The legacy jsunit tests overwrite OC.config before calling initCore
  35. * therefore we need to wait with assigning the config fallback until initCore calls initSessionHeartBeat
  36. */
  37. const loadConfig = () => {
  38. try {
  39. config = loadState('core', 'config')
  40. } catch (e) {
  41. // This fallback is just for our legacy jsunit tests since we have no way to mock loadState calls
  42. config = OC.config
  43. }
  44. }
  45. /**
  46. * session heartbeat (defaults to enabled)
  47. *
  48. * @return {boolean}
  49. */
  50. const keepSessionAlive = () => {
  51. return config.session_keepalive === undefined
  52. || !!config.session_keepalive
  53. }
  54. /**
  55. * get interval in seconds
  56. *
  57. * @return {number}
  58. */
  59. const getInterval = () => {
  60. let interval = NaN
  61. if (config.session_lifetime) {
  62. interval = Math.floor(config.session_lifetime / 2)
  63. }
  64. // minimum one minute, max 24 hours, default 15 minutes
  65. return Math.min(
  66. 24 * 3600,
  67. Math.max(
  68. 60,
  69. isNaN(interval) ? 900 : interval
  70. )
  71. )
  72. }
  73. const getToken = async () => {
  74. const url = generateUrl('/csrftoken')
  75. // Not using Axios here as Axios is not stubbable with the sinon fake server
  76. // see https://stackoverflow.com/questions/41516044/sinon-mocha-test-with-async-ajax-calls-didnt-return-promises
  77. // see js/tests/specs/coreSpec.js for the tests
  78. const resp = await $.get(url)
  79. return resp.token
  80. }
  81. const poll = async () => {
  82. try {
  83. const token = await getToken()
  84. setRequestToken(token)
  85. } catch (e) {
  86. console.error('session heartbeat failed', e)
  87. }
  88. }
  89. const startPolling = () => {
  90. const interval = setInterval(poll, getInterval() * 1000)
  91. console.info('session heartbeat polling started')
  92. return interval
  93. }
  94. const registerAutoLogout = () => {
  95. if (!config.auto_logout || !getCurrentUser()) {
  96. return
  97. }
  98. let lastActive = Date.now()
  99. window.addEventListener('mousemove', e => {
  100. lastActive = Date.now()
  101. localStorage.setItem('lastActive', lastActive)
  102. })
  103. window.addEventListener('touchstart', e => {
  104. lastActive = Date.now()
  105. localStorage.setItem('lastActive', lastActive)
  106. })
  107. window.addEventListener('storage', e => {
  108. if (e.key !== 'lastActive') {
  109. return
  110. }
  111. lastActive = e.newValue
  112. })
  113. setInterval(function() {
  114. const timeout = Date.now() - config.session_lifetime * 1000
  115. if (lastActive < timeout) {
  116. console.info('Inactivity timout reached, logging out')
  117. const logoutUrl = generateUrl('/logout') + '?requesttoken=' + encodeURIComponent(getRequestToken())
  118. window.location = logoutUrl
  119. }
  120. }, 1000)
  121. }
  122. /**
  123. * Calls the server periodically to ensure that session and CSRF
  124. * token doesn't expire
  125. */
  126. export const initSessionHeartBeat = () => {
  127. loadConfig()
  128. registerAutoLogout()
  129. if (!keepSessionAlive()) {
  130. console.info('session heartbeat disabled')
  131. return
  132. }
  133. let interval = startPolling()
  134. window.addEventListener('online', async () => {
  135. console.info('browser is online again, resuming heartbeat')
  136. interval = startPolling()
  137. try {
  138. await poll()
  139. console.info('session token successfully updated after resuming network')
  140. // Let apps know we're online and requests will have the new token
  141. emit('networkOnline', {
  142. success: true,
  143. })
  144. } catch (e) {
  145. console.error('could not update session token after resuming network', e)
  146. // Let apps know we're online but requests might have an outdated token
  147. emit('networkOnline', {
  148. success: false,
  149. })
  150. }
  151. })
  152. window.addEventListener('offline', () => {
  153. console.info('browser is offline, stopping heartbeat')
  154. // Let apps know we're offline
  155. emit('networkOffline', {})
  156. clearInterval(interval)
  157. console.info('session heartbeat polling stopped')
  158. })
  159. }