Skip to content

Commit

Permalink
fix: imported fixes (#33136)
Browse files Browse the repository at this point in the history
  • Loading branch information
julio-cfa authored Aug 23, 2024
1 parent 95ec3b3 commit ac08452
Show file tree
Hide file tree
Showing 7 changed files with 392 additions and 154 deletions.
5 changes: 5 additions & 0 deletions .changeset/orange-clocks-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates)
20 changes: 20 additions & 0 deletions apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const getModifiedHttpHeaders = (httpHeaders: Record<string, any>) => {
const modifiedHttpHeaders = { ...httpHeaders };

if ('x-auth-token' in modifiedHttpHeaders) {
modifiedHttpHeaders['x-auth-token'] = '[redacted]';
}

if (modifiedHttpHeaders.cookie) {
const cookies = modifiedHttpHeaders.cookie.split('; ');
const modifiedCookies = cookies.map((cookie: string) => {
if (cookie.startsWith('rc_token=')) {
return 'rc_token=[redacted]';
}
return cookie;
});
modifiedHttpHeaders.cookie = modifiedCookies.join('; ');
}

return modifiedHttpHeaders;
};
3 changes: 2 additions & 1 deletion apps/meteor/app/lib/server/lib/debug.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import _ from 'underscore';
import { getMethodArgs } from '../../../../server/lib/logger/logPayloads';
import { metrics } from '../../../metrics/server';
import { settings } from '../../../settings/server';
import { getModifiedHttpHeaders } from '../functions/getModifiedHttpHeaders';

const logger = new Logger('Meteor');

Expand Down Expand Up @@ -41,7 +42,7 @@ const traceConnection = (enable, filter, prefix, name, connection, userId) => {
console.log(name, {
id: connection.id,
clientAddress: connection.clientAddress,
httpHeaders: connection.httpHeaders,
httpHeaders: getModifiedHttpHeaders(connection.httpHeaders),
userId,
});
} else {
Expand Down
109 changes: 59 additions & 50 deletions apps/meteor/app/livechat/server/api/v1/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,63 +31,72 @@ import { findVisitorInfo } from '../lib/visitors';

const isAgentWithInfo = (agentObj: ILivechatAgent | { hiddenInfo: boolean }): agentObj is ILivechatAgent => !('hiddenInfo' in agentObj);

API.v1.addRoute('livechat/room', {
async get() {
// I'll temporary use check for validation, as validateParams doesnt support what's being done here
const extraCheckParams = await onCheckRoomParams({
token: String,
rid: Match.Maybe(String),
agentId: Match.Maybe(String),
});

check(this.queryParams, extraCheckParams as any);

const { token, rid, agentId, ...extraParams } = this.queryParams;

const guest = token && (await findGuest(token));
if (!guest) {
throw new Error('invalid-token');
}

if (!rid) {
const room = await LivechatRooms.findOneOpenByVisitorToken(token, {});
if (room) {
return API.v1.success({ room, newRoom: false });
}

let agent: SelectedAgent | undefined;
const agentObj = agentId && (await findAgent(agentId));
if (agentObj) {
if (isAgentWithInfo(agentObj)) {
const { username = undefined } = agentObj;
agent = { agentId, username };
} else {
agent = { agentId };
}
API.v1.addRoute(
'livechat/room',
{
rateLimiterOptions: {
numRequestsAllowed: 5,
intervalTimeInMS: 60000,
},
},
{
async get() {
// I'll temporary use check for validation, as validateParams doesnt support what's being done here
const extraCheckParams = await onCheckRoomParams({
token: String,
rid: Match.Maybe(String),
agentId: Match.Maybe(String),
});

check(this.queryParams, extraCheckParams as any);

const { token, rid, agentId, ...extraParams } = this.queryParams;

const guest = token && (await findGuest(token));
if (!guest) {
throw new Error('invalid-token');
}

const roomInfo = {
source: {
type: isWidget(this.request.headers) ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API,
},
};
if (!rid) {
const room = await LivechatRooms.findOneOpenByVisitorToken(token, {});
if (room) {
return API.v1.success({ room, newRoom: false });
}

const newRoom = await LivechatTyped.createRoom({ visitor: guest, roomInfo, agent, extraData: extraParams });
let agent: SelectedAgent | undefined;
const agentObj = agentId && (await findAgent(agentId));
if (agentObj) {
if (isAgentWithInfo(agentObj)) {
const { username = undefined } = agentObj;
agent = { agentId, username };
} else {
agent = { agentId };
}
}

return API.v1.success({
room: newRoom,
newRoom: true,
});
}
const roomInfo = {
source: {
type: isWidget(this.request.headers) ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API,
},
};

const newRoom = await LivechatTyped.createRoom({ visitor: guest, roomInfo, agent, extraData: extraParams });

const froom = await LivechatRooms.findOneOpenByRoomIdAndVisitorToken(rid, token, {});
if (!froom) {
throw new Error('invalid-room');
}
return API.v1.success({
room: newRoom,
newRoom: true,
});
}

const froom = await LivechatRooms.findOneOpenByRoomIdAndVisitorToken(rid, token, {});
if (!froom) {
throw new Error('invalid-room');
}

return API.v1.success({ room: froom, newRoom: false });
return API.v1.success({ room: froom, newRoom: false });
},
},
});
);

// Note: use this route if a visitor is closing a room
// If a RC user(like eg agent) is closing a room, use the `livechat/room.closeByUser` route
Expand Down
213 changes: 111 additions & 102 deletions apps/meteor/app/livechat/server/api/v1/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,119 +9,128 @@ import { settings } from '../../../../settings/server';
import { Livechat as LivechatTyped } from '../../lib/LivechatTyped';
import { findGuest, normalizeHttpHeaderData } from '../lib/livechat';

API.v1.addRoute('livechat/visitor', {
async post() {
check(this.bodyParams, {
visitor: Match.ObjectIncluding({
token: String,
name: Match.Maybe(String),
email: Match.Maybe(String),
department: Match.Maybe(String),
phone: Match.Maybe(String),
username: Match.Maybe(String),
customFields: Match.Maybe([
Match.ObjectIncluding({
key: String,
value: String,
overwrite: Boolean,
}),
]),
}),
});

const { customFields, id, token, name, email, department, phone, username, connectionData } = this.bodyParams.visitor;

if (!token?.trim()) {
throw new Meteor.Error('error-invalid-token', 'Token cannot be empty', { method: 'livechat/visitor' });
}

const guest = {
token,
...(id && { id }),
...(name && { name }),
...(email && { email }),
...(department && { department }),
...(username && { username }),
...(connectionData && { connectionData }),
...(phone && typeof phone === 'string' && { phone: { number: phone as string } }),
connectionData: normalizeHttpHeaderData(this.request.headers),
};

const visitor = await LivechatTyped.registerGuest(guest);
if (!visitor) {
throw new Meteor.Error('error-livechat-visitor-registration', 'Error registering visitor', {
method: 'livechat/visitor',
API.v1.addRoute(
'livechat/visitor',
{
rateLimiterOptions: {
numRequestsAllowed: 5,
intervalTimeInMS: 60000,
},
},
{
async post() {
check(this.bodyParams, {
visitor: Match.ObjectIncluding({
token: String,
name: Match.Maybe(String),
email: Match.Maybe(String),
department: Match.Maybe(String),
phone: Match.Maybe(String),
username: Match.Maybe(String),
customFields: Match.Maybe([
Match.ObjectIncluding({
key: String,
value: String,
overwrite: Boolean,
}),
]),
}),
});
}

const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
// If it's updating an existing visitor, it must also update the roomInfo
const rooms = await LivechatRooms.findOpenByVisitorToken(visitor?.token, {}, extraQuery).toArray();
await Promise.all(
rooms.map(
(room: IRoom) =>
visitor &&
LivechatTyped.saveRoomInfo(room, {
_id: visitor._id,
name: visitor.name,
phone: visitor.phone?.[0]?.phoneNumber,
livechatData: visitor.livechatData as { [k: string]: string },
}),
),
);

if (customFields && Array.isArray(customFields) && customFields.length > 0) {
const keys = customFields.map((field) => field.key);
const errors: string[] = [];

const processedKeys = await Promise.all(
await LivechatCustomField.findByIdsAndScope<Pick<ILivechatCustomField, '_id'>>(keys, 'visitor', {
projection: { _id: 1 },
})
.map(async (field) => {
const customField = customFields.find((f) => f.key === field._id);
if (!customField) {
return;
}

const { key, value, overwrite } = customField;
// TODO: Change this to Bulk update
if (!(await VisitorsRaw.updateLivechatDataByToken(token, key, value, overwrite))) {
errors.push(key);
}

return key;
})
.toArray(),
);
const { customFields, id, token, name, email, department, phone, username, connectionData } = this.bodyParams.visitor;

if (processedKeys.length !== keys.length) {
LivechatTyped.logger.warn({
msg: 'Some custom fields were not processed',
visitorId: visitor._id,
missingKeys: keys.filter((key) => !processedKeys.includes(key)),
});
if (!token?.trim()) {
throw new Meteor.Error('error-invalid-token', 'Token cannot be empty', { method: 'livechat/visitor' });
}

if (errors.length > 0) {
LivechatTyped.logger.error({
msg: 'Error updating custom fields',
visitorId: visitor._id,
errors,
const guest = {
token,
...(id && { id }),
...(name && { name }),
...(email && { email }),
...(department && { department }),
...(username && { username }),
...(connectionData && { connectionData }),
...(phone && typeof phone === 'string' && { phone: { number: phone as string } }),
connectionData: normalizeHttpHeaderData(this.request.headers),
};

const visitor = await LivechatTyped.registerGuest(guest);
if (!visitor) {
throw new Meteor.Error('error-livechat-visitor-registration', 'Error registering visitor', {
method: 'livechat/visitor',
});
throw new Error('error-updating-custom-fields');
}

return API.v1.success({ visitor: await VisitorsRaw.findOneEnabledById(visitor._id) });
}
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
// If it's updating an existing visitor, it must also update the roomInfo
const rooms = await LivechatRooms.findOpenByVisitorToken(visitor?.token, {}, extraQuery).toArray();
await Promise.all(
rooms.map(
(room: IRoom) =>
visitor &&
LivechatTyped.saveRoomInfo(room, {
_id: visitor._id,
name: visitor.name,
phone: visitor.phone?.[0]?.phoneNumber,
livechatData: visitor.livechatData as { [k: string]: string },
}),
),
);

if (!visitor) {
throw new Meteor.Error('error-saving-visitor', 'An error ocurred while saving visitor');
}
if (customFields && Array.isArray(customFields) && customFields.length > 0) {
const keys = customFields.map((field) => field.key);
const errors: string[] = [];

return API.v1.success({ visitor });
const processedKeys = await Promise.all(
await LivechatCustomField.findByIdsAndScope<Pick<ILivechatCustomField, '_id'>>(keys, 'visitor', {
projection: { _id: 1 },
})
.map(async (field) => {
const customField = customFields.find((f) => f.key === field._id);
if (!customField) {
return;
}

const { key, value, overwrite } = customField;
// TODO: Change this to Bulk update
if (!(await VisitorsRaw.updateLivechatDataByToken(token, key, value, overwrite))) {
errors.push(key);
}

return key;
})
.toArray(),
);

if (processedKeys.length !== keys.length) {
LivechatTyped.logger.warn({
msg: 'Some custom fields were not processed',
visitorId: visitor._id,
missingKeys: keys.filter((key) => !processedKeys.includes(key)),
});
}

if (errors.length > 0) {
LivechatTyped.logger.error({
msg: 'Error updating custom fields',
visitorId: visitor._id,
errors,
});
throw new Error('error-updating-custom-fields');
}

return API.v1.success({ visitor: await VisitorsRaw.findOneEnabledById(visitor._id) });
}

if (!visitor) {
throw new Meteor.Error('error-saving-visitor', 'An error ocurred while saving visitor');
}

return API.v1.success({ visitor });
},
},
});
);

API.v1.addRoute('livechat/visitor/:token', {
async get() {
Expand Down
Loading

0 comments on commit ac08452

Please sign in to comment.