mirror of
https://github.com/nextcloud/server.git
synced 2026-02-11 14:54:02 -05:00
refactor(OC): restructure session heartbeat code - use types and human reading order
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
f4966b2bf4
commit
da5828ef42
1 changed files with 111 additions and 122 deletions
|
|
@ -3,152 +3,55 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import $ from 'jquery'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import {
|
||||
fetchRequestToken,
|
||||
getRequestToken,
|
||||
} from './OC/requesttoken.ts'
|
||||
import logger from './logger.js'
|
||||
|
||||
import OC from './OC/index.js'
|
||||
import { setToken as setRequestToken, getToken as getRequestToken } from './OC/requesttoken.ts'
|
||||
|
||||
let config = null
|
||||
/**
|
||||
* The legacy jsunit tests overwrite OC.config before calling initCore
|
||||
* therefore we need to wait with assigning the config fallback until initCore calls initSessionHeartBeat
|
||||
*/
|
||||
const loadConfig = () => {
|
||||
try {
|
||||
config = loadState('core', 'config')
|
||||
} catch (e) {
|
||||
// This fallback is just for our legacy jsunit tests since we have no way to mock loadState calls
|
||||
config = OC.config
|
||||
}
|
||||
interface OcJsConfig {
|
||||
auto_logout: boolean
|
||||
session_keepalive: boolean
|
||||
session_lifetime: number
|
||||
}
|
||||
|
||||
/**
|
||||
* session heartbeat (defaults to enabled)
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
const keepSessionAlive = () => {
|
||||
return config.session_keepalive === undefined
|
||||
|| !!config.session_keepalive
|
||||
}
|
||||
|
||||
/**
|
||||
* get interval in seconds
|
||||
*
|
||||
* @return {number}
|
||||
*/
|
||||
const getInterval = () => {
|
||||
let interval = NaN
|
||||
if (config.session_lifetime) {
|
||||
interval = Math.floor(config.session_lifetime / 2)
|
||||
}
|
||||
|
||||
// minimum one minute, max 24 hours, default 15 minutes
|
||||
return Math.min(
|
||||
24 * 3600,
|
||||
Math.max(
|
||||
60,
|
||||
isNaN(interval) ? 900 : interval,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const getToken = async () => {
|
||||
const url = generateUrl('/csrftoken')
|
||||
|
||||
// Not using Axios here as Axios is not stubbable with the sinon fake server
|
||||
// see https://stackoverflow.com/questions/41516044/sinon-mocha-test-with-async-ajax-calls-didnt-return-promises
|
||||
// see js/tests/specs/coreSpec.js for the tests
|
||||
const resp = await $.get(url)
|
||||
|
||||
return resp.token
|
||||
}
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const token = await getToken()
|
||||
setRequestToken(token)
|
||||
} catch (e) {
|
||||
console.error('session heartbeat failed', e)
|
||||
}
|
||||
}
|
||||
|
||||
const startPolling = () => {
|
||||
const interval = setInterval(poll, getInterval() * 1000)
|
||||
|
||||
console.info('session heartbeat polling started')
|
||||
|
||||
return interval
|
||||
}
|
||||
|
||||
const registerAutoLogout = () => {
|
||||
if (!config.auto_logout || !getCurrentUser()) {
|
||||
return
|
||||
}
|
||||
|
||||
let lastActive = Date.now()
|
||||
window.addEventListener('mousemove', e => {
|
||||
lastActive = Date.now()
|
||||
localStorage.setItem('lastActive', lastActive)
|
||||
})
|
||||
|
||||
window.addEventListener('touchstart', e => {
|
||||
lastActive = Date.now()
|
||||
localStorage.setItem('lastActive', lastActive)
|
||||
})
|
||||
|
||||
window.addEventListener('storage', e => {
|
||||
if (e.key !== 'lastActive') {
|
||||
return
|
||||
}
|
||||
lastActive = e.newValue
|
||||
})
|
||||
|
||||
let intervalId = 0
|
||||
const logoutCheck = () => {
|
||||
const timeout = Date.now() - config.session_lifetime * 1000
|
||||
if (lastActive < timeout) {
|
||||
clearTimeout(intervalId)
|
||||
console.info('Inactivity timout reached, logging out')
|
||||
const logoutUrl = generateUrl('/logout') + '?requesttoken=' + encodeURIComponent(getRequestToken())
|
||||
window.location = logoutUrl
|
||||
}
|
||||
}
|
||||
intervalId = setInterval(logoutCheck, 1000)
|
||||
}
|
||||
const {
|
||||
auto_logout: autoLogout,
|
||||
session_keepalive: keepSessionAlive,
|
||||
session_lifetime: seesionLifetime,
|
||||
} = loadState<OcJsConfig>('core', 'config')
|
||||
|
||||
/**
|
||||
* Calls the server periodically to ensure that session and CSRF
|
||||
* token doesn't expire
|
||||
*/
|
||||
export const initSessionHeartBeat = () => {
|
||||
loadConfig()
|
||||
|
||||
export function initSessionHeartBeat() {
|
||||
registerAutoLogout()
|
||||
|
||||
if (!keepSessionAlive()) {
|
||||
console.info('session heartbeat disabled')
|
||||
if (!keepSessionAlive) {
|
||||
logger.info('Session heartbeat disabled')
|
||||
return
|
||||
}
|
||||
let interval = startPolling()
|
||||
|
||||
let interval = startPolling()
|
||||
window.addEventListener('online', async () => {
|
||||
console.info('browser is online again, resuming heartbeat')
|
||||
logger.info('Browser is online again, resuming heartbeat')
|
||||
|
||||
interval = startPolling()
|
||||
try {
|
||||
await poll()
|
||||
console.info('session token successfully updated after resuming network')
|
||||
logger.info('Session token successfully updated after resuming network')
|
||||
|
||||
// Let apps know we're online and requests will have the new token
|
||||
emit('networkOnline', {
|
||||
success: true,
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('could not update session token after resuming network', e)
|
||||
} catch (error) {
|
||||
logger.error('could not update session token after resuming network', { error })
|
||||
|
||||
// Let apps know we're online but requests might have an outdated token
|
||||
emit('networkOnline', {
|
||||
|
|
@ -156,13 +59,99 @@ export const initSessionHeartBeat = () => {
|
|||
})
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
console.info('browser is offline, stopping heartbeat')
|
||||
logger.info('Browser is offline, stopping heartbeat')
|
||||
|
||||
// Let apps know we're offline
|
||||
emit('networkOffline', {})
|
||||
|
||||
clearInterval(interval)
|
||||
console.info('session heartbeat polling stopped')
|
||||
logger.info('Session heartbeat polling stopped')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interval in seconds
|
||||
*/
|
||||
function getInterval(): number {
|
||||
const interval = seesionLifetime
|
||||
? Math.floor(seesionLifetime / 2)
|
||||
: 900
|
||||
|
||||
// minimum one minute, max 24 hours, default 15 minutes
|
||||
return Math.min(
|
||||
24 * 3600,
|
||||
Math.max(
|
||||
60,
|
||||
interval,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll the CSRF token for changes.
|
||||
* This will also extend the current session if needed.
|
||||
*/
|
||||
async function poll() {
|
||||
try {
|
||||
await fetchRequestToken()
|
||||
} catch (error) {
|
||||
logger.error('session heartbeat failed', { error })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start an window interval with the polling as the callback.
|
||||
*
|
||||
* @return The interval id
|
||||
*/
|
||||
function startPolling(): number {
|
||||
const interval = window.setInterval(poll, getInterval() * 1000)
|
||||
|
||||
logger.info('session heartbeat polling started')
|
||||
return interval
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled this will register event listeners to track if a user is active.
|
||||
* If not the user will be automatically logged out after the configured IDLE time.
|
||||
*/
|
||||
function registerAutoLogout() {
|
||||
if (!autoLogout || !getCurrentUser()) {
|
||||
return
|
||||
}
|
||||
|
||||
let lastActive = Date.now()
|
||||
window.addEventListener('mousemove', () => {
|
||||
lastActive = Date.now()
|
||||
localStorage.setItem('lastActive', JSON.stringify(lastActive))
|
||||
})
|
||||
|
||||
window.addEventListener('touchstart', () => {
|
||||
lastActive = Date.now()
|
||||
localStorage.setItem('lastActive', JSON.stringify(lastActive))
|
||||
})
|
||||
|
||||
window.addEventListener('storage', (event) => {
|
||||
if (event.key !== 'lastActive') {
|
||||
return
|
||||
}
|
||||
if (event.newValue === null) {
|
||||
return
|
||||
}
|
||||
lastActive = JSON.parse(event.newValue)
|
||||
})
|
||||
|
||||
let intervalId = 0
|
||||
const logoutCheck = () => {
|
||||
const timeout = Date.now() - seesionLifetime * 1000
|
||||
if (lastActive < timeout) {
|
||||
clearTimeout(intervalId)
|
||||
logger.info('Inactivity timout reached, logging out')
|
||||
const logoutUrl = generateUrl('/logout') + '?requesttoken=' + encodeURIComponent(getRequestToken())
|
||||
window.location.href = logoutUrl
|
||||
}
|
||||
}
|
||||
intervalId = window.setInterval(logoutCheck, 1000)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue