|
|
|
|
@ -3,166 +3,155 @@ |
|
|
|
|
* 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' |
|
|
|
|
|
|
|
|
|
interface OcJsConfig { |
|
|
|
|
auto_logout: boolean |
|
|
|
|
session_keepalive: boolean |
|
|
|
|
session_lifetime: number |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
import OC from './OC/index.js' |
|
|
|
|
import { setToken as setRequestToken, getToken as getRequestToken } from './OC/requesttoken.ts' |
|
|
|
|
const { |
|
|
|
|
auto_logout: autoLogout, |
|
|
|
|
session_keepalive: keepSessionAlive, |
|
|
|
|
session_lifetime: seesionLifetime, |
|
|
|
|
} = loadState<OcJsConfig>('core', 'config') |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
* Calls the server periodically to ensure that session and CSRF |
|
|
|
|
* token doesn't expire |
|
|
|
|
*/ |
|
|
|
|
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 |
|
|
|
|
export function initSessionHeartBeat() { |
|
|
|
|
registerAutoLogout() |
|
|
|
|
|
|
|
|
|
if (!keepSessionAlive) { |
|
|
|
|
logger.info('Session heartbeat disabled') |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* session heartbeat (defaults to enabled) |
|
|
|
|
* |
|
|
|
|
* @return {boolean} |
|
|
|
|
*/ |
|
|
|
|
const keepSessionAlive = () => { |
|
|
|
|
return config.session_keepalive === undefined |
|
|
|
|
|| !!config.session_keepalive |
|
|
|
|
let interval = startPolling() |
|
|
|
|
window.addEventListener('online', async () => { |
|
|
|
|
logger.info('Browser is online again, resuming heartbeat') |
|
|
|
|
|
|
|
|
|
interval = startPolling() |
|
|
|
|
try { |
|
|
|
|
await poll() |
|
|
|
|
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 (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', { |
|
|
|
|
success: false, |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
window.addEventListener('offline', () => { |
|
|
|
|
logger.info('Browser is offline, stopping heartbeat') |
|
|
|
|
|
|
|
|
|
// Let apps know we're offline
|
|
|
|
|
emit('networkOffline', {}) |
|
|
|
|
|
|
|
|
|
clearInterval(interval) |
|
|
|
|
logger.info('Session heartbeat polling stopped') |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* get interval in seconds |
|
|
|
|
* |
|
|
|
|
* @return {number} |
|
|
|
|
* Get interval in seconds |
|
|
|
|
*/ |
|
|
|
|
const getInterval = () => { |
|
|
|
|
let interval = NaN |
|
|
|
|
if (config.session_lifetime) { |
|
|
|
|
interval = Math.floor(config.session_lifetime / 2) |
|
|
|
|
} |
|
|
|
|
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, |
|
|
|
|
isNaN(interval) ? 900 : interval, |
|
|
|
|
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 () => { |
|
|
|
|
/** |
|
|
|
|
* Poll the CSRF token for changes. |
|
|
|
|
* This will also extend the current session if needed. |
|
|
|
|
*/ |
|
|
|
|
async function poll() { |
|
|
|
|
try { |
|
|
|
|
const token = await getToken() |
|
|
|
|
setRequestToken(token) |
|
|
|
|
} catch (e) { |
|
|
|
|
console.error('session heartbeat failed', e) |
|
|
|
|
await fetchRequestToken() |
|
|
|
|
} catch (error) { |
|
|
|
|
logger.error('session heartbeat failed', { error }) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const startPolling = () => { |
|
|
|
|
const interval = setInterval(poll, getInterval() * 1000) |
|
|
|
|
|
|
|
|
|
console.info('session heartbeat polling started') |
|
|
|
|
/** |
|
|
|
|
* 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 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const registerAutoLogout = () => { |
|
|
|
|
if (!config.auto_logout || !getCurrentUser()) { |
|
|
|
|
/** |
|
|
|
|
* 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', e => { |
|
|
|
|
window.addEventListener('mousemove', () => { |
|
|
|
|
lastActive = Date.now() |
|
|
|
|
localStorage.setItem('lastActive', lastActive) |
|
|
|
|
localStorage.setItem('lastActive', JSON.stringify(lastActive)) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
window.addEventListener('touchstart', e => { |
|
|
|
|
window.addEventListener('touchstart', () => { |
|
|
|
|
lastActive = Date.now() |
|
|
|
|
localStorage.setItem('lastActive', lastActive) |
|
|
|
|
localStorage.setItem('lastActive', JSON.stringify(lastActive)) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
window.addEventListener('storage', e => { |
|
|
|
|
if (e.key !== 'lastActive') { |
|
|
|
|
window.addEventListener('storage', (event) => { |
|
|
|
|
if (event.key !== 'lastActive') { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
lastActive = e.newValue |
|
|
|
|
if (event.newValue === null) { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
lastActive = JSON.parse(event.newValue) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
let intervalId = 0 |
|
|
|
|
const logoutCheck = () => { |
|
|
|
|
const timeout = Date.now() - config.session_lifetime * 1000 |
|
|
|
|
const timeout = Date.now() - seesionLifetime * 1000 |
|
|
|
|
if (lastActive < timeout) { |
|
|
|
|
clearTimeout(intervalId) |
|
|
|
|
console.info('Inactivity timout reached, logging out') |
|
|
|
|
logger.info('Inactivity timout reached, logging out') |
|
|
|
|
const logoutUrl = generateUrl('/logout') + '?requesttoken=' + encodeURIComponent(getRequestToken()) |
|
|
|
|
window.location = logoutUrl |
|
|
|
|
window.location.href = logoutUrl |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
intervalId = setInterval(logoutCheck, 1000) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Calls the server periodically to ensure that session and CSRF |
|
|
|
|
* token doesn't expire |
|
|
|
|
*/ |
|
|
|
|
export const initSessionHeartBeat = () => { |
|
|
|
|
loadConfig() |
|
|
|
|
|
|
|
|
|
registerAutoLogout() |
|
|
|
|
|
|
|
|
|
if (!keepSessionAlive()) { |
|
|
|
|
console.info('session heartbeat disabled') |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
let interval = startPolling() |
|
|
|
|
|
|
|
|
|
window.addEventListener('online', async () => { |
|
|
|
|
console.info('browser is online again, resuming heartbeat') |
|
|
|
|
interval = startPolling() |
|
|
|
|
try { |
|
|
|
|
await poll() |
|
|
|
|
console.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) |
|
|
|
|
|
|
|
|
|
// Let apps know we're online but requests might have an outdated token
|
|
|
|
|
emit('networkOnline', { |
|
|
|
|
success: false, |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
window.addEventListener('offline', () => { |
|
|
|
|
console.info('browser is offline, stopping heartbeat') |
|
|
|
|
|
|
|
|
|
// Let apps know we're offline
|
|
|
|
|
emit('networkOffline', {}) |
|
|
|
|
|
|
|
|
|
clearInterval(interval) |
|
|
|
|
console.info('session heartbeat polling stopped') |
|
|
|
|
}) |
|
|
|
|
intervalId = window.setInterval(logoutCheck, 1000) |
|
|
|
|
} |
|
|
|
|
|