diff --git a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts index 3beb3639cf3b0..3880a6e0384a6 100644 --- a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts +++ b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts @@ -1,4 +1,4 @@ -import type { ISession, ISessionDevice, ISocketConnectionLogged, IUser } from '@rocket.chat/core-typings'; +import type { ISession, ISessionDevice, IUser, LoginSessionPayload, LogoutSessionPayload } from '@rocket.chat/core-typings'; import { cronJobs } from '@rocket.chat/cron'; import { Logger } from '@rocket.chat/logger'; import { Sessions, Users, aggregates } from '@rocket.chat/models'; @@ -8,7 +8,6 @@ import UAParser from 'ua-parser-js'; import { UAParserMobile, UAParserDesktop } from './UAParserCustom'; import { getMostImportantRole } from '../../../../lib/roles/getMostImportantRole'; -import { getClientAddress } from '../../../../server/lib/getClientAddress'; import { sauEvents } from '../../../../server/services/sauMonitor/events'; type DateObj = { day: number; month: number; year: number }; @@ -111,21 +110,24 @@ export class SAUMonitorClass { return; } - sauEvents.on('accounts.login', async ({ userId, connection }) => { - if (!this.isRunning()) { - return; - } + sauEvents.on( + 'sau.accounts.login', + async ({ userId, instanceId, userAgent, loginToken, connectionId, clientAddress, host }: LoginSessionPayload) => { + if (!this.isRunning()) { + return; + } - const roles = await getUserRoles(userId); + const roles = await getUserRoles(userId); - const mostImportantRole = getMostImportantRole(roles); + const mostImportantRole = getMostImportantRole(roles); - const loginAt = new Date(); - const params = { userId, roles, mostImportantRole, loginAt, ...getDateObj() }; - await this._handleSession(connection, params); - }); + const loginAt = new Date(); + const params = { roles, mostImportantRole, loginAt, ...getDateObj() }; + await this._handleSession({ userId, instanceId, userAgent, loginToken, connectionId, clientAddress, host }, params); + }, + ); - sauEvents.on('accounts.logout', async ({ userId, connection }) => { + sauEvents.on('sau.accounts.logout', async ({ userId, sessionId }: LogoutSessionPayload) => { if (!this.isRunning()) { return; } @@ -135,7 +137,6 @@ export class SAUMonitorClass { return; } - const { id: sessionId } = connection; if (!sessionId) { logger.warn({ msg: "Received 'accounts.logout' event without 'sessionId'" }); return; @@ -157,10 +158,20 @@ export class SAUMonitorClass { } private async _handleSession( - connection: ISocketConnectionLogged, - params: Pick, + { userId, instanceId, userAgent, loginToken, connectionId, clientAddress, host }: LoginSessionPayload, + params: Pick, ): Promise { - const data = this._getConnectionInfo(connection, params); + const data: Omit = { + userId, + loginToken, + ip: clientAddress, + host, + sessionId: connectionId, + instanceId, + type: 'session', + ...this._getUserAgentInfo(userAgent), + ...params, + }; if (!data) { return; @@ -221,37 +232,7 @@ export class SAUMonitorClass { .join(''); } - private _getConnectionInfo( - connection: ISocketConnectionLogged, - params: Pick, - ): Omit | undefined { - if (!connection) { - return; - } - - const ip = getClientAddress(connection); - - const host = connection.httpHeaders?.host ?? ''; - - return { - type: 'session', - sessionId: connection.id, - instanceId: connection.instanceId, - ...(connection.loginToken && { loginToken: connection.loginToken }), - ip, - host, - ...this._getUserAgentInfo(connection), - ...params, - }; - } - - private _getUserAgentInfo(connection: ISocketConnectionLogged): { device: ISessionDevice } | undefined { - if (!connection?.httpHeaders?.['user-agent']) { - return; - } - - const uaString = connection.httpHeaders['user-agent']; - + private _getUserAgentInfo(uaString: string): { device: ISessionDevice } | undefined { // TODO define a type for "result" below // | UAParser.IResult // | { device: { type: string; model?: string }; browser: undefined; os: undefined; app: { name: string; version: string } } diff --git a/apps/meteor/ee/server/lib/deviceManagement/session.ts b/apps/meteor/ee/server/lib/deviceManagement/session.ts index c5b286c9dff2c..f5d8acb4c0843 100644 --- a/apps/meteor/ee/server/lib/deviceManagement/session.ts +++ b/apps/meteor/ee/server/lib/deviceManagement/session.ts @@ -32,14 +32,14 @@ const uaParser = async ( }; export const listenSessionLogin = () => { - return deviceManagementEvents.on('device-login', async ({ userId, connection }) => { + return deviceManagementEvents.on('device-login', async ({ userId, userAgent, loginToken, clientAddress }) => { const deviceEnabled = settings.get('Device_Management_Enable_Login_Emails'); if (!deviceEnabled) { return; } - if (connection.loginToken) { + if (loginToken) { return; } @@ -67,11 +67,7 @@ export const listenSessionLogin = () => { emails: [{ address: email }], } = user; - const userAgentString = - connection.httpHeaders instanceof Headers - ? (connection.httpHeaders.get('user-agent') ?? '') - : (connection.httpHeaders['user-agent'] ?? ''); - const { browser, os, device, cpu, app } = await uaParser(userAgentString); + const { browser, os, device, cpu, app } = await uaParser(userAgent); const mailData = { name, @@ -81,7 +77,7 @@ export const listenSessionLogin = () => { deviceInfo: `${device.type || t('Device_Management_Device_Unknown')} ${device.vendor || ''} ${device.model || ''} ${ cpu.architecture || '' }`, - ipInfo: connection.clientAddress, + ipInfo: clientAddress, userAgent: '', date: moment().format(String(dateFormat)), }; @@ -105,7 +101,7 @@ export const listenSessionLogin = () => { mailData.deviceInfo = `Desktop App ${cpu.architecture || ''}`; break; default: - mailData.userAgent = connection.httpHeaders['user-agent'] || ''; + mailData.userAgent = userAgent || ''; break; } diff --git a/apps/meteor/server/hooks/sauMonitorHooks.ts b/apps/meteor/server/hooks/sauMonitorHooks.ts index 3137ab41237a1..122ce09886852 100644 --- a/apps/meteor/server/hooks/sauMonitorHooks.ts +++ b/apps/meteor/server/hooks/sauMonitorHooks.ts @@ -1,10 +1,13 @@ import type { IncomingHttpHeaders } from 'http'; +import type { LoginSessionPayload, LogoutSessionPayload, DeviceLoginPayload } from '@rocket.chat/core-typings'; import { InstanceStatus } from '@rocket.chat/instance-status'; import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import type { ILoginAttempt } from '../../app/authentication/server/ILoginAttempt'; +import { getClientAddress } from '../lib/getClientAddress'; +import { getHeader } from '../lib/getHeader'; import { deviceManagementEvents } from '../services/device-management/events'; import { sauEvents } from '../services/sauMonitor/events'; @@ -19,36 +22,51 @@ Accounts.onLogin((info: ILoginAttempt) => { } const { resume } = methodArguments.find((arg) => 'resume' in arg) ?? {}; + const loginToken = resume ? Accounts._hashLoginToken(resume) : ''; + const instanceId = InstanceStatus.id(); + const userId = info.user._id; + const connectionId = info.connection.id; + const clientAddress = getClientAddress(info.connection); + const userAgent = getHeader(httpHeaders, 'user-agent'); + const host = getHeader(httpHeaders, 'host'); - const eventObject = { - userId: info.user._id, - connection: { - ...info.connection, - ...(resume && { loginToken: Accounts._hashLoginToken(resume) }), - instanceId: InstanceStatus.id(), - httpHeaders: httpHeaders as IncomingHttpHeaders, - }, + const loginEventObject: LoginSessionPayload = { + userId, + instanceId, + userAgent, + loginToken, + connectionId, + clientAddress, + host, }; - sauEvents.emit('accounts.login', eventObject); - deviceManagementEvents.emit('device-login', eventObject); + sauEvents.emit('sau.accounts.login', loginEventObject); + + const deviceLoginEventObject: DeviceLoginPayload = { + userId, + userAgent, + loginToken, + clientAddress, + }; + deviceManagementEvents.emit('device-login', deviceLoginEventObject); }); Accounts.onLogout((info) => { - const { httpHeaders } = info.connection; - if (!info.user) { return; } - sauEvents.emit('accounts.logout', { + + const logoutEventObject: LogoutSessionPayload = { userId: info.user._id, - connection: { instanceId: InstanceStatus.id(), ...info.connection, httpHeaders: httpHeaders as IncomingHttpHeaders }, - }); + sessionId: info.connection.id, + }; + + sauEvents.emit('sau.accounts.logout', logoutEventObject); }); Meteor.onConnection((connection) => { connection.onClose(async () => { const { httpHeaders } = connection; - sauEvents.emit('socket.disconnected', { + sauEvents.emit('sau.socket.disconnected', { instanceId: InstanceStatus.id(), ...connection, httpHeaders: httpHeaders as IncomingHttpHeaders, @@ -58,6 +76,9 @@ Meteor.onConnection((connection) => { Meteor.onConnection((connection) => { const { httpHeaders } = connection; - - sauEvents.emit('socket.connected', { instanceId: InstanceStatus.id(), ...connection, httpHeaders: httpHeaders as IncomingHttpHeaders }); + sauEvents.emit('sau.socket.connected', { + instanceId: InstanceStatus.id(), + ...connection, + httpHeaders: httpHeaders as IncomingHttpHeaders, + }); }); diff --git a/apps/meteor/server/lib/getHeader.ts b/apps/meteor/server/lib/getHeader.ts new file mode 100644 index 0000000000000..cd8a7eaa781a6 --- /dev/null +++ b/apps/meteor/server/lib/getHeader.ts @@ -0,0 +1,11 @@ +export const getHeader = (headers: unknown, key: string): string => { + if (!headers) { + return ''; + } + + if (typeof (headers as any).get === 'function') { + return (headers as Headers).get(key) ?? ''; + } + + return (headers as Record)[key] || ''; +}; diff --git a/apps/meteor/server/services/device-management/events.ts b/apps/meteor/server/services/device-management/events.ts index 5f99c63da472f..b40df77de2c83 100644 --- a/apps/meteor/server/services/device-management/events.ts +++ b/apps/meteor/server/services/device-management/events.ts @@ -1,6 +1,6 @@ -import type { ISocketConnectionLogged } from '@rocket.chat/core-typings'; +import type { DeviceLoginPayload } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; export const deviceManagementEvents = new Emitter<{ - 'device-login': { userId: string; connection: ISocketConnectionLogged }; + 'device-login': DeviceLoginPayload; }>(); diff --git a/apps/meteor/server/services/device-management/service.ts b/apps/meteor/server/services/device-management/service.ts index 75f3639088922..0592473a97997 100644 --- a/apps/meteor/server/services/device-management/service.ts +++ b/apps/meteor/server/services/device-management/service.ts @@ -2,6 +2,8 @@ import type { IDeviceManagementService } from '@rocket.chat/core-services'; import { ServiceClassInternal } from '@rocket.chat/core-services'; import { deviceManagementEvents } from './events'; +import { getClientAddress } from '../../lib/getClientAddress'; +import { getHeader } from '../../lib/getHeader'; export class DeviceManagementService extends ServiceClassInternal implements IDeviceManagementService { protected name = 'device-management'; @@ -9,9 +11,11 @@ export class DeviceManagementService extends ServiceClassInternal implements IDe constructor() { super(); - this.onEvent('accounts.login', async (data) => { + this.onEvent('accounts.login', async ({ userId, connection }) => { + const clientAddress = getClientAddress(connection); + const userAgent = getHeader(connection.httpHeaders, 'user-agent'); // TODO need to add loginToken to data - deviceManagementEvents.emit('device-login', data); + deviceManagementEvents.emit('device-login', { userId, userAgent, clientAddress }); }); } } diff --git a/apps/meteor/server/services/sauMonitor/events.ts b/apps/meteor/server/services/sauMonitor/events.ts index 44b971a91a70e..8ffb3560a6e6d 100644 --- a/apps/meteor/server/services/sauMonitor/events.ts +++ b/apps/meteor/server/services/sauMonitor/events.ts @@ -1,9 +1,9 @@ -import type { ISocketConnection, ISocketConnectionLogged } from '@rocket.chat/core-typings'; +import type { ISocketConnection, LoginSessionPayload, LogoutSessionPayload } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; export const sauEvents = new Emitter<{ - 'accounts.login': { userId: string; connection: ISocketConnectionLogged }; - 'accounts.logout': { userId: string; connection: ISocketConnection }; - 'socket.connected': ISocketConnection; - 'socket.disconnected': ISocketConnection; + 'sau.accounts.login': LoginSessionPayload; + 'sau.accounts.logout': LogoutSessionPayload; + 'sau.socket.connected': ISocketConnection; + 'sau.socket.disconnected': ISocketConnection; }>(); diff --git a/apps/meteor/server/services/sauMonitor/service.ts b/apps/meteor/server/services/sauMonitor/service.ts index a0b36bb201633..724bf5f199ff1 100644 --- a/apps/meteor/server/services/sauMonitor/service.ts +++ b/apps/meteor/server/services/sauMonitor/service.ts @@ -2,8 +2,11 @@ import { ServiceClassInternal } from '@rocket.chat/core-services'; import type { ISAUMonitorService } from '@rocket.chat/core-services'; +import { InstanceStatus } from '@rocket.chat/instance-status'; import { sauEvents } from './events'; +import { getClientAddress } from '../../lib/getClientAddress'; +import { getHeader } from '../../lib/getHeader'; export class SAUMonitorService extends ServiceClassInternal implements ISAUMonitorService { protected name = 'sau-monitor'; @@ -11,22 +14,28 @@ export class SAUMonitorService extends ServiceClassInternal implements ISAUMonit constructor() { super(); - this.onEvent('accounts.login', async (data) => { - sauEvents.emit('accounts.login', data); + this.onEvent('accounts.login', async ({ userId, connection }) => { + const instanceId = InstanceStatus.id(); + const connectionId = connection.id; + const clientAddress = getClientAddress(connection); + const userAgent = getHeader(connection.httpHeaders, 'user-agent'); + const host = getHeader(connection.httpHeaders, 'host'); + + sauEvents.emit('sau.accounts.login', { userId, instanceId, connectionId, clientAddress, userAgent, host }); }); - this.onEvent('accounts.logout', async (data) => { - sauEvents.emit('accounts.logout', data); + this.onEvent('accounts.logout', async ({ userId, connection }) => { + sauEvents.emit('sau.accounts.logout', { userId, sessionId: connection.id }); }); this.onEvent('socket.disconnected', async (data) => { // console.log('socket.disconnected', data); - sauEvents.emit('socket.disconnected', data); + sauEvents.emit('sau.socket.disconnected', data); }); this.onEvent('socket.connected', async (data) => { // console.log('socket.connected', data); - sauEvents.emit('socket.connected', data); + sauEvents.emit('sau.socket.connected', data); }); } } diff --git a/packages/core-typings/src/DeviceLoginPayload.ts b/packages/core-typings/src/DeviceLoginPayload.ts new file mode 100644 index 0000000000000..b7eb03fb8bab8 --- /dev/null +++ b/packages/core-typings/src/DeviceLoginPayload.ts @@ -0,0 +1,6 @@ +export type DeviceLoginPayload = { + userId: string; + userAgent: string; + loginToken?: string; + clientAddress: string; +}; diff --git a/packages/core-typings/src/LoginSessionPayload.ts b/packages/core-typings/src/LoginSessionPayload.ts new file mode 100644 index 0000000000000..0936592912452 --- /dev/null +++ b/packages/core-typings/src/LoginSessionPayload.ts @@ -0,0 +1,9 @@ +export type LoginSessionPayload = { + userId: string; + instanceId: string; + userAgent: string; + loginToken?: string; + connectionId: string; + clientAddress: string; + host: string; +}; diff --git a/packages/core-typings/src/LogoutSessionPayload.ts b/packages/core-typings/src/LogoutSessionPayload.ts new file mode 100644 index 0000000000000..a4c0b55b80362 --- /dev/null +++ b/packages/core-typings/src/LogoutSessionPayload.ts @@ -0,0 +1,4 @@ +export type LogoutSessionPayload = { + userId?: string; + sessionId?: string; +}; diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index befca6827c4cf..233742926ef23 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -145,4 +145,8 @@ export * from './IAbacAttribute'; export * from './Abac'; export * from './ServerAudit/IAuditServerAbacAction'; +export * from './LoginSessionPayload'; +export * from './DeviceLoginPayload'; +export * from './LogoutSessionPayload'; + export { schemas } from './Ajv';