diff --git a/apps/meteor/app/api/server/index.ts b/apps/meteor/app/api/server/index.ts index 9ee5c04b9339f..971c0476ad5b0 100644 --- a/apps/meteor/app/api/server/index.ts +++ b/apps/meteor/app/api/server/index.ts @@ -13,7 +13,6 @@ import './v1/channels'; import './v1/chat'; import './v1/cloud'; import './v1/commands'; -import './v1/dns'; import './v1/e2e'; import './v1/emoji-custom'; import './v1/groups'; diff --git a/apps/meteor/app/api/server/v1/dns.ts b/apps/meteor/app/api/server/v1/dns.ts deleted file mode 100644 index cd1e20fbe4ad5..0000000000000 --- a/apps/meteor/app/api/server/v1/dns.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { Match, check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { resolveSRV, resolveTXT } from '../../../federation/server/functions/resolveDNS'; -import { API } from '../api'; - -/** - * @openapi - * /api/v1/dns.resolve.srv: - * get: - * description: Resolves DNS service records (SRV records) for a hostname - * security: - * $ref: '#/security/authenticated' - * parameters: - * - name: url - * in: query - * description: The hostname - * required: true - * schema: - * type: string - * example: open.rocket.chat - * responses: - * 200: - * description: The resolved records - * content: - * application/json: - * schema: - * allOf: - * - $ref: '#/components/schemas/ApiSuccessV1' - * - type: object - * properties: - * resolved: - * type: object - * properties: - * target: - * type: string - * priority: - * type: number - * weight: - * type: number - * port: - * type: number - * default: - * description: Unexpected error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ApiFailureV1' - */ -API.v1.addRoute( - 'dns.resolve.srv', - { authRequired: true }, - { - async get() { - check( - this.queryParams, - Match.ObjectIncluding({ - url: String, - }), - ); - - const { url } = this.queryParams; - if (!url) { - throw new Meteor.Error('error-missing-param', 'The required "url" param is missing.'); - } - - const resolved = await resolveSRV(url); - - return API.v1.success({ resolved }); - }, - }, -); - -/** - * @openapi - * /api/v1/dns.resolve.txt: - * get: - * description: Resolves DNS text records (TXT records) for a hostname - * security: - * $ref: '#/security/authenticated' - * parameters: - * - name: url - * in: query - * description: The hostname - * required: true - * schema: - * type: string - * example: open.rocket.chat - * responses: - * 200: - * description: The resolved records - * content: - * application/json: - * schema: - * allOf: - * - $ref: '#/components/schemas/ApiSuccessV1' - * - type: object - * properties: - * resolved: - * type: string - * default: - * description: Unexpected error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ApiFailureV1' - */ -API.v1.addRoute( - 'dns.resolve.txt', - { authRequired: true }, - { - async post() { - check( - this.queryParams, - Match.ObjectIncluding({ - url: String, - }), - ); - - const { url } = this.queryParams; - if (!url) { - throw new Meteor.Error('error-missing-param', 'The required "url" param is missing.'); - } - - const resolved = await resolveTXT(url); - - return API.v1.success({ resolved }); - }, - }, -); diff --git a/apps/meteor/app/federation/README.md b/apps/meteor/app/federation/README.md deleted file mode 100644 index e8f08c1023325..0000000000000 --- a/apps/meteor/app/federation/README.md +++ /dev/null @@ -1 +0,0 @@ -##Rocket.Chat Federation \ No newline at end of file diff --git a/apps/meteor/app/federation/server/constants.ts b/apps/meteor/app/federation/server/constants.ts deleted file mode 100644 index 1dadf121b9e51..0000000000000 --- a/apps/meteor/app/federation/server/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const STATUS_ENABLED = 'Enabled'; -export const STATUS_REGISTERING = 'Registering with Hub...'; -export const STATUS_ERROR_REGISTERING = 'Could not register with Hub'; -export const STATUS_DISABLED = 'Disabled'; diff --git a/apps/meteor/app/federation/server/endpoints/dispatch.js b/apps/meteor/app/federation/server/endpoints/dispatch.js deleted file mode 100644 index 65a675722bba2..0000000000000 --- a/apps/meteor/app/federation/server/endpoints/dispatch.js +++ /dev/null @@ -1,633 +0,0 @@ -import { api } from '@rocket.chat/core-services'; -import { eventTypes } from '@rocket.chat/core-typings'; -import { FederationServers, FederationRoomEvents, Rooms, Messages, Subscriptions, Users, ReadReceipts } from '@rocket.chat/models'; -import { removeEmpty } from '@rocket.chat/tools'; -import EJSON from 'ejson'; - -import { API } from '../../../api/server'; -import { FileUpload } from '../../../file-upload/server'; -import { deleteRoom } from '../../../lib/server/functions/deleteRoom'; -import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { - notifyOnMessageChange, - notifyOnRoomChanged, - notifyOnRoomChangedById, - notifyOnSubscriptionChanged, - notifyOnSubscriptionChangedById, -} from '../../../lib/server/lib/notifyListener'; -import { notifyUsersOnMessage } from '../../../lib/server/lib/notifyUsersOnMessage'; -import { sendAllNotifications } from '../../../lib/server/lib/sendNotificationsOnMessage'; -import { processThreads } from '../../../threads/server/hooks/aftersavemessage'; -import { getUpload, requestEventsFromLatest } from '../handler'; -import { contextDefinitions } from '../lib/context'; -import { decryptIfNeeded } from '../lib/crypt'; -import { getFederationDomain } from '../lib/getFederationDomain'; -import { isFederationEnabled } from '../lib/isFederationEnabled'; -import { serverLogger } from '../lib/logger'; -import { normalizers } from '../normalizers'; - -const eventHandlers = { - // - // PING - // - async [eventTypes.PING]() { - return { - success: true, - }; - }, - - // - // GENESIS - // - async [eventTypes.GENESIS](event) { - switch (event.data.contextType) { - case contextDefinitions.ROOM.type: - const eventResult = await FederationRoomEvents.addEvent(event.context, event); - - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { - data: { room }, - } = event; - - // Check if room exists - const persistedRoom = await Rooms.findOne({ _id: room._id }); - - if (persistedRoom) { - // Update the federation - await Rooms.updateOne({ _id: persistedRoom._id }, { $set: { federation: room.federation } }); - - // Notify watch.rooms listener - void notifyOnRoomChangedById(room._id); - } else { - // Denormalize room - const denormalizedRoom = normalizers.denormalizeRoom(room); - - // Create the room - const insertedRoom = await Rooms.insertOne(denormalizedRoom); - - // Notify watch.rooms listener - void notifyOnRoomChangedById(insertedRoom.insertedId); - } - } - return eventResult; - } - }, - - // - // ROOM_DELETE - // - async [eventTypes.ROOM_DELETE](event) { - const { - data: { roomId }, - } = event; - - // Check if room exists - const persistedRoom = await Rooms.findOne({ _id: roomId }); - - if (persistedRoom) { - // Delete the room - await deleteRoom(roomId); - - // Notify watch.rooms listener - void notifyOnRoomChanged(persistedRoom, 'removed'); - } - - // Remove all room events - await FederationRoomEvents.removeRoomEvents(roomId); - - return { - success: true, - }; - }, - - // - // ROOM_ADD_USER - // - async [eventTypes.ROOM_ADD_USER](event) { - const eventResult = await FederationRoomEvents.addEvent(event.context, event); - - // We only want to refresh the server list and update the room federation array if something changed - let federationAltered = false; - - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { - data: { roomId, user, subscription, domainsAfterAdd }, - } = event; - - // Check if user exists - const persistedUser = await Users.findOne({ _id: user._id }); - - if (persistedUser) { - // Update the federation, if its not already set (if it's set, this is likely an event being reprocessed) - if (!persistedUser.federation && user.federation) { - await Users.updateOne({ _id: persistedUser._id }, { $set: { federation: removeEmpty(user.federation) } }); - federationAltered = true; - } - } else { - // Denormalize user - const denormalizedUser = normalizers.denormalizeUser(user); - - // Create the user - await Users.insertOne(denormalizedUser); - federationAltered = true; - } - - // Check if subscription exists - const persistedSubscription = await Subscriptions.findOne({ _id: subscription._id }); - - try { - if (persistedSubscription) { - // Update the federation, if its not already set (if it's set, this is likely an event being reprocessed - if (!persistedSubscription.federation && subscription.federation) { - await Subscriptions.updateOne( - { _id: persistedSubscription._id }, - { $set: { federation: removeEmpty(subscription.federation) } }, - ); - federationAltered = true; - } - } else { - // Denormalize subscription - const denormalizedSubscription = normalizers.denormalizeSubscription(subscription); - - // Create the subscription - const { insertedId } = await Subscriptions.insertOne(removeEmpty(denormalizedSubscription)); - if (insertedId) { - void notifyOnSubscriptionChangedById(insertedId); - } - federationAltered = true; - } - } catch (ex) { - serverLogger.debug(`unable to create subscription for user ( ${user._id} ) in room (${roomId})`); - } - - // Refresh the servers list - if (federationAltered) { - await FederationServers.refreshServers(); - - // Update the room's federation property - await Rooms.updateOne({ _id: roomId }, { $set: { 'federation.domains': domainsAfterAdd } }); - - // Notify watch.rooms listener - void notifyOnRoomChangedById(roomId); - } - } - - return eventResult; - }, - - // - // ROOM_REMOVE_USER - // - async [eventTypes.ROOM_REMOVE_USER](event) { - const eventResult = await FederationRoomEvents.addEvent(event.context, event); - - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { - data: { roomId, user, domainsAfterRemoval }, - } = event; - - // Remove the user's subscription - const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(roomId, user._id); - if (deletedSubscription) { - void notifyOnSubscriptionChanged(deletedSubscription, 'removed'); - } - - // Refresh the servers list - await FederationServers.refreshServers(); - - // Update the room's federation property - await Rooms.updateOne({ _id: roomId }, { $set: { 'federation.domains': domainsAfterRemoval } }); - - // Notify watch.rooms listener - void notifyOnRoomChangedById(roomId); - } - - return eventResult; - }, - - // - // ROOM_USER_LEFT - // - async [eventTypes.ROOM_USER_LEFT](event) { - const eventResult = await FederationRoomEvents.addEvent(event.context, event); - - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { - data: { roomId, user, domainsAfterRemoval }, - } = event; - - // Remove the user's subscription - const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(roomId, user._id); - if (deletedSubscription) { - void notifyOnSubscriptionChanged(deletedSubscription, 'removed'); - } - - // Refresh the servers list - await FederationServers.refreshServers(); - - // Update the room's federation property - await Rooms.updateOne({ _id: roomId }, { $set: { 'federation.domains': domainsAfterRemoval } }); - - // Notify watch.rooms listener - void notifyOnRoomChangedById(roomId); - } - - return eventResult; - }, - - // - // ROOM_MESSAGE - // - async [eventTypes.ROOM_MESSAGE](event) { - const eventResult = await FederationRoomEvents.addEvent(event.context, event); - - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { - data: { message }, - } = event; - - // Check if message exists - const persistedMessage = await Messages.findOne({ _id: message._id }); - let messageForNotification; - - if (persistedMessage) { - // Update the federation - if (!persistedMessage.federation) { - await Messages.updateOne({ _id: persistedMessage._id }, { $set: { federation: message.federation } }); - messageForNotification = { ...persistedMessage, federation: message.federation }; - } - } else { - // Load the room - const room = await Rooms.findOneById(message.rid); - - // Denormalize message - const denormalizedMessage = normalizers.denormalizeMessage(message); - - // Is there a file? - if (denormalizedMessage.file) { - const fileStore = FileUpload.getStore('Uploads'); - - const { - federation: { origin }, - } = denormalizedMessage; - - const { upload, buffer } = await getUpload(origin, denormalizedMessage.file._id); - - const oldUploadId = upload._id; - - // Normalize upload - delete upload._id; - upload.rid = denormalizedMessage.rid; - upload.userId = denormalizedMessage.u._id; - upload.federation = { - _id: denormalizedMessage.file._id, - origin, - }; - - await fileStore.insert(upload, buffer); - - // Update the message's file - denormalizedMessage.file._id = upload._id; - - // Update the message's attachments dependent on type - for (const attachment of denormalizedMessage.attachments) { - attachment.title_link = attachment.title_link.replace(oldUploadId, upload._id); - if (/^image\/.+/.test(denormalizedMessage.file.type)) { - attachment.image_url = attachment.image_url.replace(oldUploadId, upload._id); - } else if (/^audio\/.+/.test(denormalizedMessage.file.type)) { - attachment.audio_url = attachment.audio_url.replace(oldUploadId, upload._id); - } else if (/^video\/.+/.test(denormalizedMessage.file.type)) { - attachment.video_url = attachment.video_url.replace(oldUploadId, upload._id); - } - } - } - - // Create the message - try { - await Messages.insertOne(denormalizedMessage); - - await processThreads(denormalizedMessage, room); - - const roomUpdater = Rooms.getUpdater(); - await notifyUsersOnMessage(denormalizedMessage, room, roomUpdater); - if (roomUpdater.hasChanges()) { - await Rooms.updateFromUpdater({ _id: room._id }, roomUpdater); - } - - sendAllNotifications(denormalizedMessage, room); - messageForNotification = denormalizedMessage; - } catch (err) { - serverLogger.debug(`Error on creating message: ${message._id}`); - } - } - if (messageForNotification) { - void notifyOnMessageChange({ - id: messageForNotification._id, - data: messageForNotification, - }); - } - } - - return eventResult; - }, - - // - // ROOM_EDIT_MESSAGE - // - async [eventTypes.ROOM_EDIT_MESSAGE](event) { - const eventResult = await FederationRoomEvents.addEvent(event.context, event); - - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { - data: { message }, - } = event; - - // Check if message exists - const persistedMessage = await Messages.findOne({ _id: message._id }); - - if (!persistedMessage) { - eventResult.success = false; - eventResult.reason = 'missingMessageToEdit'; - } else { - // Update the message - await Messages.updateOne({ _id: persistedMessage._id }, { $set: { msg: message.msg, federation: message.federation } }); - void notifyOnMessageChange({ - id: persistedMessage._id, - data: { - ...persistedMessage, - msg: message.msg, - federation: message.federation, - }, - }); - } - } - - return eventResult; - }, - - // - // ROOM_DELETE_MESSAGE - // - async [eventTypes.ROOM_DELETE_MESSAGE](event) { - const eventResult = await FederationRoomEvents.addEvent(event.context, event); - - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { - data: { roomId, messageId }, - } = event; - - // Remove the message - await Messages.removeById(messageId); - await ReadReceipts.removeByMessageId(messageId); - - // Notify the room - void api.broadcast('notify.deleteMessage', roomId, { _id: messageId }); - } - - return eventResult; - }, - - // - // ROOM_SET_MESSAGE_REACTION - // - async [eventTypes.ROOM_SET_MESSAGE_REACTION](event) { - const eventResult = await FederationRoomEvents.addEvent(event.context, event); - - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { - data: { messageId, username, reaction }, - } = event; - - // Get persisted message - const persistedMessage = await Messages.findOne({ _id: messageId }); - - // Make sure reactions exist - persistedMessage.reactions = persistedMessage.reactions || {}; - - let reactionObj = persistedMessage.reactions[reaction]; - - // If there are no reactions of that type, add it - if (!reactionObj) { - reactionObj = { - usernames: [username], - }; - } else { - // Otherwise, add the username - reactionObj.usernames.push(username); - reactionObj.usernames = [...new Set(reactionObj.usernames)]; - } - - // Update the property - await Messages.updateOne({ _id: messageId }, { $set: { [`reactions.${reaction}`]: reactionObj } }); - void notifyOnMessageChange({ - id: persistedMessage._id, - data: { - ...persistedMessage, - reactions: { - ...persistedMessage.reactions, - [reaction]: reactionObj, - }, - }, - }); - } - - return eventResult; - }, - - // - // ROOM_UNSET_MESSAGE_REACTION - // - async [eventTypes.ROOM_UNSET_MESSAGE_REACTION](event) { - const eventResult = await FederationRoomEvents.addEvent(event.context, event); - - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { - data: { messageId, username, reaction }, - } = event; - - // Get persisted message - const persistedMessage = await Messages.findOne({ _id: messageId }); - - // Make sure reactions exist - persistedMessage.reactions = persistedMessage.reactions || {}; - - // If there are no reactions of that type, ignore - if (!persistedMessage.reactions[reaction]) { - return eventResult; - } - - const reactionObj = persistedMessage.reactions[reaction]; - - // Get the username index on the list - const usernameIdx = reactionObj.usernames.indexOf(username); - - // If the index is not found, ignore - if (usernameIdx === -1) { - return eventResult; - } - - // Remove the username from the given reaction - reactionObj.usernames.splice(usernameIdx, 1); - - // If there are no more users for that reaction, remove the property - if (reactionObj.usernames.length === 0) { - await Messages.updateOne({ _id: messageId }, { $unset: { [`reactions.${reaction}`]: 1 } }); - } else { - // Otherwise, update the property - await Messages.updateOne({ _id: messageId }, { $set: { [`reactions.${reaction}`]: reactionObj } }); - } - void notifyOnMessageChange({ - id: persistedMessage._id, - data: { - ...persistedMessage, - reactions: { - ...persistedMessage.reactions, - [reaction]: reactionObj, - }, - }, - }); - } - - return eventResult; - }, - - // - // ROOM_MUTE_USER - // - async [eventTypes.ROOM_MUTE_USER](event) { - const eventResult = await FederationRoomEvents.addEvent(event.context, event); - - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { - data: { roomId, user }, - } = event; - - // Denormalize user - const denormalizedUser = normalizers.denormalizeUser(user); - - // Mute user - await Rooms.muteUsernameByRoomId(roomId, denormalizedUser.username); - - // Broadcast the unmute event - void notifyOnRoomChangedById(roomId); - } - - return eventResult; - }, - - // - // ROOM_UNMUTE_USER - // - async [eventTypes.ROOM_UNMUTE_USER](event) { - const eventResult = await FederationRoomEvents.addEvent(event.context, event); - - // If the event was successfully added, handle the event locally - if (eventResult.success) { - const { - data: { roomId, user }, - } = event; - - // Denormalize user - const denormalizedUser = normalizers.denormalizeUser(user); - - // Unmute user - await Rooms.unmuteMutedUsernameByRoomId(roomId, denormalizedUser.username); - - // Broadcast the unmute event - void notifyOnRoomChangedById(roomId); - } - - return eventResult; - }, -}; - -API.v1.addRoute( - 'federation.events.dispatch', - { authRequired: false, rateLimiterOptions: { numRequestsAllowed: 30, intervalTimeInMS: 1000 } }, - { - async post() { - /* - The legacy federation has been deprecated for over a year - and no longer receives any updates. This feature also has - relevant security issues that weren't addressed. - Workspaces should migrate to the newer matrix federation. - */ - apiDeprecationLogger.endpoint(this.request.route, '8.0.0', this.response, 'Use Matrix Federation instead.'); - - if (!process.env.ENABLE_INSECURE_LEGACY_FEDERATION) { - return API.v1.failure('Deprecated. ENABLE_INSECURE_LEGACY_FEDERATION environment variable is needed to enable it.'); - } - - if (!isFederationEnabled()) { - return API.v1.failure('Federation not enabled'); - } - - // - // Decrypt the payload if needed - let payload; - - try { - payload = await decryptIfNeeded(this.request, this.bodyParams); - } catch (err) { - return API.v1.failure('Could not decrypt payload'); - } - - // - // Convert from EJSON - const { events } = EJSON.fromJSONValue(payload); - - serverLogger.debug({ msg: 'federation.events.dispatch', events }); - - // Loop over received events - for (const event of events) { - /* eslint-disable no-await-in-loop */ - - let eventResult; - - if (eventHandlers[event.type]) { - eventResult = await eventHandlers[event.type](event); - } - - // If there was an error handling the event, take action - if (!eventResult || !eventResult.success) { - try { - serverLogger.debug({ - msg: 'federation.events.dispatch => Event has missing parents', - event, - }); - - await requestEventsFromLatest( - event.origin, - getFederationDomain(), - contextDefinitions.defineType(event), - event.context, - eventResult.latestEventIds, - ); - - // And stop handling the events - break; - } catch (err) { - serverLogger.error({ msg: 'dispatch', event, eventResult, err }); - - throw err; - } - } - - /* eslint-enable no-await-in-loop */ - } - - // Respond - return API.v1.success(); - }, - }, -); diff --git a/apps/meteor/app/federation/server/endpoints/index.ts b/apps/meteor/app/federation/server/endpoints/index.ts deleted file mode 100644 index a8a11611b82b7..0000000000000 --- a/apps/meteor/app/federation/server/endpoints/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import './dispatch'; -import './requestFromLatest'; -import './uploads'; -import './users'; diff --git a/apps/meteor/app/federation/server/endpoints/requestFromLatest.js b/apps/meteor/app/federation/server/endpoints/requestFromLatest.js deleted file mode 100644 index 8dca41d3f4426..0000000000000 --- a/apps/meteor/app/federation/server/endpoints/requestFromLatest.js +++ /dev/null @@ -1,90 +0,0 @@ -import { FederationRoomEvents } from '@rocket.chat/models'; -import EJSON from 'ejson'; - -import { API } from '../../../api/server'; -import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { dispatchEvents } from '../handler'; -import { decryptIfNeeded } from '../lib/crypt'; -import { isFederationEnabled } from '../lib/isFederationEnabled'; -import { serverLogger } from '../lib/logger'; - -API.v1.addRoute( - 'federation.events.requestFromLatest', - { authRequired: false }, - { - async post() { - /* - The legacy federation has been deprecated for over a year - and no longer receives any updates. This feature also has - relevant security issues that weren't addressed. - Workspaces should migrate to the newer matrix federation. - */ - apiDeprecationLogger.endpoint(this.request.route, '8.0.0', this.response, 'Use Matrix Federation instead.'); - - if (!process.env.ENABLE_INSECURE_LEGACY_FEDERATION) { - return API.v1.failure('Deprecated. ENABLE_INSECURE_LEGACY_FEDERATION environment variable is needed to enable it.'); - } - - if (!isFederationEnabled()) { - return API.v1.failure('Federation not enabled'); - } - - // - // Decrypt the payload if needed - let payload; - - try { - payload = await decryptIfNeeded(this.request, this.bodyParams); - } catch (err) { - return API.v1.failure('Could not decrypt payload'); - } - - const { fromDomain, contextType, contextQuery, latestEventIds } = EJSON.fromJSONValue(payload); - - serverLogger.debug({ - msg: 'federation.events.requestFromLatest', - contextType, - contextQuery, - latestEventIds, - }); - - let EventsModel; - - // Define the model for the context - switch (contextType) { - case 'room': - EventsModel = FederationRoomEvents; - break; - } - - let missingEvents = []; - - if (latestEventIds.length) { - // Get the oldest event from the latestEventIds - const oldestEvent = await EventsModel.findOne({ _id: { $in: latestEventIds } }, { $sort: { timestamp: 1 } }); - - if (!oldestEvent) { - return; - } - - // Get all the missing events on this context, after the oldest one - missingEvents = await EventsModel.find( - { - _id: { $nin: latestEventIds }, - context: contextQuery, - timestamp: { $gte: oldestEvent.timestamp }, - }, - { sort: { timestamp: 1 } }, - ).toArray(); - } else { - // If there are no latest events, send all of them - missingEvents = await EventsModel.find({ context: contextQuery }, { sort: { timestamp: 1 } }).toArray(); - } - - // Dispatch all the events, on the same request - await dispatchEvents([fromDomain], missingEvents); - - return API.v1.success(); - }, - }, -); diff --git a/apps/meteor/app/federation/server/endpoints/uploads.js b/apps/meteor/app/federation/server/endpoints/uploads.js deleted file mode 100644 index 10c513b3741ca..0000000000000 --- a/apps/meteor/app/federation/server/endpoints/uploads.js +++ /dev/null @@ -1,42 +0,0 @@ -import { Uploads } from '@rocket.chat/models'; - -import { API } from '../../../api/server'; -import { FileUpload } from '../../../file-upload/server'; -import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { isFederationEnabled } from '../lib/isFederationEnabled'; - -API.v1.addRoute( - 'federation.uploads', - { authRequired: false }, - { - async get() { - /* - The legacy federation has been deprecated for over a year - and no longer receives any updates. This feature also has - relevant security issues that weren't addressed. - Workspaces should migrate to the newer matrix federation. - */ - apiDeprecationLogger.endpoint(this.request.route, '8.0.0', this.response, 'Use Matrix Federation instead.'); - - if (!process.env.ENABLE_INSECURE_LEGACY_FEDERATION) { - return API.v1.failure('Deprecated. ENABLE_INSECURE_LEGACY_FEDERATION environment variable is needed to enable it.'); - } - - if (!isFederationEnabled()) { - return API.v1.failure('Federation not enabled'); - } - - const { upload_id } = this.queryParams; - - const upload = await Uploads.findOneById(upload_id); - - if (!upload) { - return API.v1.failure('There is no such file in this server'); - } - - const buffer = await FileUpload.getBuffer(upload); - - return API.v1.success({ upload, buffer }); - }, - }, -); diff --git a/apps/meteor/app/federation/server/endpoints/users.js b/apps/meteor/app/federation/server/endpoints/users.js deleted file mode 100644 index fba9b1642e8f6..0000000000000 --- a/apps/meteor/app/federation/server/endpoints/users.js +++ /dev/null @@ -1,87 +0,0 @@ -import { Users } from '@rocket.chat/models'; - -import { API } from '../../../api/server'; -import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { isFederationEnabled } from '../lib/isFederationEnabled'; -import { serverLogger } from '../lib/logger'; -import { normalizers } from '../normalizers'; - -const userFields = { _id: 1, username: 1, type: 1, emails: 1, name: 1 }; - -API.v1.addRoute( - 'federation.users.search', - { authRequired: false }, - { - async get() { - /* - The legacy federation has been deprecated for over a year - and no longer receives any updates. This feature also has - relevant security issues that weren't addressed. - Workspaces should migrate to the newer matrix federation. - */ - apiDeprecationLogger.endpoint(this.request.route, '8.0.0', this.response, 'Use Matrix Federation instead.'); - - if (!process.env.ENABLE_INSECURE_LEGACY_FEDERATION) { - return API.v1.failure('Deprecated. ENABLE_INSECURE_LEGACY_FEDERATION environment variable is needed to enable it.'); - } - - if (!isFederationEnabled()) { - return API.v1.failure('Federation not enabled'); - } - - const { username, domain } = this.queryParams; - - serverLogger.debug(`federation.users.search => username=${username} domain=${domain}`); - - const query = { - type: 'user', - $or: [{ name: username }, { username }, { 'emails.address': `${username}@${domain}` }], - }; - - let users = await Users.find(query, { projection: userFields }).toArray(); - - users = await normalizers.normalizeAllUsers(users); - - return API.v1.success({ users }); - }, - }, -); - -API.v1.addRoute( - 'federation.users.getByUsername', - { authRequired: false }, - { - async get() { - /* - The legacy federation has been deprecated for over a year - and no longer receives any updates. This feature also has - relevant security issues that weren't addressed. - Workspaces should migrate to the newer matrix federation. - */ - apiDeprecationLogger.endpoint(this.request.route, '8.0.0', this.response, 'Use Matrix Federation instead.'); - - if (!process.env.ENABLE_INSECURE_LEGACY_FEDERATION) { - return API.v1.failure('Deprecated. ENABLE_INSECURE_LEGACY_FEDERATION environment variable is needed to enable it.'); - } - - if (!isFederationEnabled()) { - return API.v1.failure('Federation not enabled'); - } - - const { username } = this.queryParams; - - serverLogger.debug(`federation.users.getByUsername => username=${username}`); - - const query = { - type: 'user', - username, - }; - - let user = await Users.findOne(query, { projection: userFields }); - - user = await normalizers.normalizeUser(user); - - return API.v1.success({ user }); - }, - }, -); diff --git a/apps/meteor/app/federation/server/functions/addUser.js b/apps/meteor/app/federation/server/functions/addUser.js deleted file mode 100644 index 0cddb226d263a..0000000000000 --- a/apps/meteor/app/federation/server/functions/addUser.js +++ /dev/null @@ -1,36 +0,0 @@ -import { FederationServers, Users } from '@rocket.chat/models'; -import { removeEmpty } from '@rocket.chat/tools'; -import { Meteor } from 'meteor/meteor'; - -import { getUserByUsername } from '../handler'; -import * as federationErrors from './errors'; - -export async function addUser(query) { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'addUser' }); - } - - const user = await getUserByUsername(query); - - if (!user) { - throw federationErrors.userNotFound(query); - } - - let userId = user._id; - - try { - // Create the local user - userId = await Users.create(removeEmpty(user)); - - // Refresh the servers list - await FederationServers.refreshServers(); - } catch (err) { - // This might get called twice by the createDirectMessage method - // so we need to handle the situation accordingly - if (err.code !== 11000) { - throw err; - } - } - - return Users.findOne({ _id: userId }); -} diff --git a/apps/meteor/app/federation/server/functions/dashboard.js b/apps/meteor/app/federation/server/functions/dashboard.js deleted file mode 100644 index bc9812f4de949..0000000000000 --- a/apps/meteor/app/federation/server/functions/dashboard.js +++ /dev/null @@ -1,47 +0,0 @@ -import { FederationServers, FederationRoomEvents, Users } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -export async function getStatistics() { - const numberOfEvents = await FederationRoomEvents.estimatedDocumentCount(); - const numberOfFederatedUsers = await Users.countRemote(); - const numberOfServers = await FederationServers.estimatedDocumentCount(); - - return { numberOfEvents, numberOfFederatedUsers, numberOfServers }; -} - -export async function federationGetOverviewData() { - if (!Meteor.userId()) { - throw new Meteor.Error('not-authorized'); - } - - const { numberOfEvents, numberOfFederatedUsers, numberOfServers } = await getStatistics(); - - return { - data: [ - { - title: 'Number_of_events', - value: numberOfEvents, - }, - { - title: 'Number_of_federated_users', - value: numberOfFederatedUsers, - }, - { - title: 'Number_of_federated_servers', - value: numberOfServers, - }, - ], - }; -} - -export async function federationGetServers() { - if (!Meteor.userId()) { - throw new Meteor.Error('not-authorized'); - } - - const servers = await FederationServers.find().toArray(); - - return { - data: servers, - }; -} diff --git a/apps/meteor/app/federation/server/functions/errors.js b/apps/meteor/app/federation/server/functions/errors.js deleted file mode 100644 index a456e44beea2c..0000000000000 --- a/apps/meteor/app/federation/server/functions/errors.js +++ /dev/null @@ -1,11 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -export const disabled = (method) => new Meteor.Error('federation-error-disabled', 'Federation disabled', { method }); - -export const userNotFound = (query) => new Meteor.Error('federation-user-not-found', `Could not find federated users using "${query}"`); - -export const peerNotFoundUsingDNS = (method) => - new Meteor.Error('federation-error-peer-no-found-using-dns', 'Could not find the peer using DNS or Hub', { method }); - -export const peerCouldNotBeRegisteredWithHub = (method) => - new Meteor.Error('federation-error-peer-could-not-register-with-hub', 'Could not register the peer using the Hub', { method }); diff --git a/apps/meteor/app/federation/server/functions/helpers.ts b/apps/meteor/app/federation/server/functions/helpers.ts deleted file mode 100644 index 54df33cdfa2d3..0000000000000 --- a/apps/meteor/app/federation/server/functions/helpers.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { isDirectMessageRoom } from '@rocket.chat/core-typings'; -import type { ISubscription, IUser, IRoom } from '@rocket.chat/core-typings'; -import { Settings, Users, Subscriptions } from '@rocket.chat/models'; - -import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; -import { STATUS_ENABLED, STATUS_REGISTERING } from '../constants'; - -export const getNameAndDomain = (fullyQualifiedName: string): string[] => fullyQualifiedName.split('@'); - -export const isFullyQualified = (name: string): boolean => name.indexOf('@') !== -1; - -export async function isRegisteringOrEnabled(): Promise { - const value = await Settings.getValueById('FEDERATION_Status'); - return typeof value === 'string' && [STATUS_ENABLED, STATUS_REGISTERING].includes(value); -} - -export async function updateStatus(status: string): Promise { - // No need to call ws listener because current function is called on startup - await Settings.updateValueById('FEDERATION_Status', status); -} - -export async function updateEnabled(enabled: boolean): Promise { - (await Settings.updateValueById('FEDERATION_Enabled', enabled)).modifiedCount && void notifyOnSettingChangedById('FEDERATION_Enabled'); -} - -export const checkRoomType = (room: IRoom): boolean => room.t === 'p' || room.t === 'd'; -export const checkRoomDomainsLength = (domains: unknown[]): boolean => domains.length <= Number(process.env.FEDERATED_DOMAINS_LENGTH ?? 10); - -export const hasExternalDomain = ({ federation }: { federation: { origin: string; domains: string[] } }): boolean => { - // same test as isFederated(room) - if (!federation) { - return false; - } - - return federation.domains.some((domain) => domain !== federation.origin); -}; - -export const isLocalUser = ({ federation }: { federation: { origin: string } }, localDomain: string): boolean => - !federation || federation.origin === localDomain; - -export const getFederatedRoomData = async ( - room: IRoom, -): Promise<{ - hasFederatedUser: boolean; - users: IUser[]; - subscriptions: { [k: string]: ISubscription } | undefined; -}> => { - if (isDirectMessageRoom(room)) { - // Check if there is a federated user on this room - - return { - users: [], - hasFederatedUser: room.usernames.some(isFullyQualified), - subscriptions: undefined, - }; - } - - // Find all subscriptions of this room - const s = await Subscriptions.findByRoomIdWhenUsernameExists(room._id).toArray(); - const subscriptions = s.reduce( - (acc, s) => { - acc[s.u._id] = s; - return acc; - }, - {} as { [k: string]: ISubscription }, - ); - - // Get all user ids - const userIds = Object.keys(subscriptions); - - // Load all the users - const users = await Users.findUsersWithUsernameByIds(userIds).toArray(); - - // Check if there is a federated user on this room - const hasFederatedUser = users.some((u) => u.username && isFullyQualified(u.username)); - - return { - hasFederatedUser, - users, - subscriptions, - }; -}; diff --git a/apps/meteor/app/federation/server/functions/resolveDNS.ts b/apps/meteor/app/federation/server/functions/resolveDNS.ts deleted file mode 100644 index 3ba9bc8350eb6..0000000000000 --- a/apps/meteor/app/federation/server/functions/resolveDNS.ts +++ /dev/null @@ -1,16 +0,0 @@ -import dns from 'dns'; -import util from 'util'; - -const dnsResolveSRV = util.promisify(dns.resolveSrv); -const dnsResolveTXT = util.promisify(dns.resolveTxt); - -export const resolveSRV = async (url: string): Promise & { target: dns.SrvRecord['name'] }> => { - const [{ name, ...resolved }] = await dnsResolveSRV(url); - return { target: name, ...resolved }; -}; - -export const resolveTXT = async (url: string): Promise => { - const [resolved] = await dnsResolveTXT(url); - - return Array.isArray(resolved) ? resolved.join('') : resolved; -}; diff --git a/apps/meteor/app/federation/server/handler/index.ts b/apps/meteor/app/federation/server/handler/index.ts deleted file mode 100644 index f7a3ae53ec298..0000000000000 --- a/apps/meteor/app/federation/server/handler/index.ts +++ /dev/null @@ -1,99 +0,0 @@ -import qs from 'querystring'; - -import { disabled } from '../functions/errors'; -import { federationRequestToPeer } from '../lib/http'; -import { isFederationEnabled } from '../lib/isFederationEnabled'; -import { clientLogger } from '../lib/logger'; - -export async function federationSearchUsers(query: string) { - if (!isFederationEnabled()) { - throw disabled('client.searchUsers'); - } - - clientLogger.debug({ msg: 'searchUsers', query }); - - const [username, peerDomain] = query.split('@'); - - const uri = `/api/v1/federation.users.search?${qs.stringify({ username, domain: peerDomain })}`; - - const { - data: { users }, - } = await federationRequestToPeer('GET', peerDomain, uri); - - return users; -} - -export async function getUserByUsername(query: string) { - if (!isFederationEnabled()) { - throw disabled('client.searchUsers'); - } - - clientLogger.debug({ msg: 'getUserByUsername', query }); - - const [username, peerDomain] = query.split('@'); - - const uri = `/api/v1/federation.users.getByUsername?${qs.stringify({ username })}`; - - const { - data: { user }, - } = await federationRequestToPeer('GET', peerDomain, uri); - - return user; -} - -export async function requestEventsFromLatest( - domain: string, - fromDomain: string, - contextType: unknown, - contextQuery: unknown, - latestEventIds: unknown, -) { - if (!isFederationEnabled()) { - throw disabled('client.requestEventsFromLatest'); - } - - clientLogger.debug({ - msg: 'requestEventsFromLatest', - domain, - contextType, - contextQuery, - latestEventIds, - }); - - const uri = '/api/v1/federation.events.requestFromLatest'; - - await federationRequestToPeer('POST', domain, uri, { - fromDomain, - contextType, - contextQuery, - latestEventIds, - }); -} - -export async function dispatchEvents(domains: string[], events: unknown[]) { - if (!isFederationEnabled()) { - throw disabled('client.dispatchEvents'); - } - - domains = [...new Set(domains)]; - - clientLogger.debug({ msg: 'dispatchEvents', domains, events }); - - const uri = '/api/v1/federation.events.dispatch'; - - for await (const domain of domains) { - await federationRequestToPeer('POST', domain, uri, { events }, { ignoreErrors: true }); - } -} - -export async function dispatchEvent(domains: string[], event: unknown) { - await dispatchEvents([...new Set(domains)], [event]); -} - -export async function getUpload(domain: string, fileId: string) { - const { - data: { upload, buffer }, - } = await federationRequestToPeer('GET', domain, `/api/v1/federation.uploads?${qs.stringify({ upload_id: fileId })}`); - - return { upload, buffer: Buffer.from(buffer) }; -} diff --git a/apps/meteor/app/federation/server/hooks/afterAddedToRoom.js b/apps/meteor/app/federation/server/hooks/afterAddedToRoom.js deleted file mode 100644 index ebf793b2d7794..0000000000000 --- a/apps/meteor/app/federation/server/hooks/afterAddedToRoom.js +++ /dev/null @@ -1,92 +0,0 @@ -import { FederationRoomEvents, Subscriptions } from '@rocket.chat/models'; - -import { getFederatedRoomData, hasExternalDomain, isLocalUser, checkRoomType, checkRoomDomainsLength } from '../functions/helpers'; -import { dispatchEvent } from '../handler'; -import { getFederationDomain } from '../lib/getFederationDomain'; -import { clientLogger } from '../lib/logger'; -import { normalizers } from '../normalizers'; -import { doAfterCreateRoom } from './afterCreateRoom'; - -async function afterAddedToRoom(involvedUsers, room) { - const { user: addedUser } = involvedUsers; - - const localDomain = getFederationDomain(); - - if (!hasExternalDomain(room) && isLocalUser(addedUser, localDomain)) { - return involvedUsers; - } - - clientLogger.debug({ msg: 'afterAddedToRoom', involvedUsers, room }); - - // If there are not federated users on this room, ignore it - const { users, subscriptions } = await getFederatedRoomData(room); - - // Load the subscription - const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, addedUser._id); - - try { - // If the room is not on the allowed types, ignore - if (!checkRoomType(room)) { - throw new Error('Channels cannot be federated'); - } - - // - // Check if the room is already federated, if it is not, create the genesis event - // - if (!room.federation) { - // - // Create the room with everything - // - - await doAfterCreateRoom(room, users, subscriptions); - } else { - // - // Normalize the room's federation status - // - - // Get the users domains - const domainsAfterAdd = []; - users.forEach((user) => { - if (user.hasOwnProperty('federation') && !domainsAfterAdd.includes(user.federation.origin)) { - domainsAfterAdd.push(user.federation.origin); - } - }); - - // Check if the number of domains is allowed - if (!checkRoomDomainsLength(domainsAfterAdd)) { - throw new Error(`Cannot federate rooms with more than ${process.env.FEDERATED_DOMAINS_LENGTH || 10} domains`); - } - - // - // Create the user add event - // - - const normalizedSourceUser = await normalizers.normalizeUser(addedUser); - const normalizedSourceSubscription = normalizers.normalizeSubscription(subscription); - - const addUserEvent = await FederationRoomEvents.createAddUserEvent( - localDomain, - room._id, - normalizedSourceUser, - normalizedSourceSubscription, - domainsAfterAdd, - ); - - // Dispatch the events - dispatchEvent(domainsAfterAdd, addUserEvent); - } - } catch (err) { - // Remove the user subscription from the room - Subscriptions.remove({ _id: subscription._id }); - - clientLogger.error({ msg: 'afterAddedToRoom => Could not add user:', err }); - } - - return involvedUsers; -} - -export const definition = { - hook: 'afterAddedToRoom', - callback: afterAddedToRoom, - id: 'federation-after-added-to-room', -}; diff --git a/apps/meteor/app/federation/server/hooks/afterCreateDirectRoom.js b/apps/meteor/app/federation/server/hooks/afterCreateDirectRoom.js deleted file mode 100644 index eeca4215095cd..0000000000000 --- a/apps/meteor/app/federation/server/hooks/afterCreateDirectRoom.js +++ /dev/null @@ -1,72 +0,0 @@ -import { FederationRoomEvents, Subscriptions } from '@rocket.chat/models'; - -import { deleteRoom } from '../../../lib/server/functions/deleteRoom'; -import { isFullyQualified } from '../functions/helpers'; -import { dispatchEvents } from '../handler'; -import { getFederationDomain } from '../lib/getFederationDomain'; -import { clientLogger } from '../lib/logger'; -import { normalizers } from '../normalizers'; - -async function afterCreateDirectRoom(room, extras) { - clientLogger.debug({ msg: 'afterCreateDirectRoom', room, extras }); - - // If the room is federated, ignore - if (room.federation) { - return room; - } - - // Check if there is a federated user on this direct room - const hasFederatedUser = room.usernames.some(isFullyQualified); - - // If there are not federated users on this room, ignore it - if (!hasFederatedUser) { - return room; - } - - try { - // - // Genesis - // - - // Normalize room - const normalizedRoom = normalizers.normalizeRoom(room); - - // Ensure a genesis event for this room - const genesisEvent = await FederationRoomEvents.createGenesisEvent(getFederationDomain(), normalizedRoom); - - const events = await Promise.all( - extras.members.map(async (member) => { - const normalizedMember = await normalizers.normalizeUser(member); - - const sourceSubscription = await Subscriptions.findOne({ - 'rid': normalizedRoom._id, - 'u._id': normalizedMember._id, - }); - const normalizedSourceSubscription = normalizers.normalizeSubscription(sourceSubscription); - - // Build the user event - return FederationRoomEvents.createAddUserEvent( - getFederationDomain(), - normalizedRoom._id, - normalizedMember, - normalizedSourceSubscription, - ); - }), - ); - - // Dispatch the events - await dispatchEvents(normalizedRoom.federation.domains, [genesisEvent, ...events]); - } catch (err) { - await deleteRoom(room._id); - - clientLogger.error({ msg: 'afterCreateDirectRoom => Could not create federated room:', err }); - } - - return room; -} - -export const definition = { - hook: 'afterCreateDirectRoom', - callback: afterCreateDirectRoom, - id: 'federation-after-create-direct-room', -}; diff --git a/apps/meteor/app/federation/server/hooks/afterCreateRoom.js b/apps/meteor/app/federation/server/hooks/afterCreateRoom.js deleted file mode 100644 index 2b4679b492124..0000000000000 --- a/apps/meteor/app/federation/server/hooks/afterCreateRoom.js +++ /dev/null @@ -1,109 +0,0 @@ -import { FederationRoomEvents, Users, Subscriptions } from '@rocket.chat/models'; - -import { deleteRoom } from '../../../lib/server/functions/deleteRoom'; -import { checkRoomType, checkRoomDomainsLength } from '../functions/helpers'; -import { dispatchEvents } from '../handler'; -import { getFederationDomain } from '../lib/getFederationDomain'; -import { clientLogger } from '../lib/logger'; -import { normalizers } from '../normalizers'; - -export async function doAfterCreateRoom(room, users, subscriptions) { - const normalizedUsers = []; - - // - // Add user events - // - const addUserEvents = []; - - for await (const user of users) { - /* eslint-disable no-await-in-loop */ - - const subscription = subscriptions[user._id]; - - const normalizedSourceUser = await normalizers.normalizeUser(user); - const normalizedSourceSubscription = normalizers.normalizeSubscription(subscription); - - normalizedUsers.push(normalizedSourceUser); - - const addUserEvent = await FederationRoomEvents.createAddUserEvent( - getFederationDomain(), - room._id, - normalizedSourceUser, - normalizedSourceSubscription, - ); - - addUserEvents.push(addUserEvent); - - /* eslint-enable no-await-in-loop */ - } - - // - // Genesis - // - - // Normalize room - const normalizedRoom = normalizers.normalizeRoom(room, normalizedUsers); - - // Check if the number of domains is allowed - if (!checkRoomDomainsLength(normalizedRoom.federation.domains)) { - throw new Error(`Cannot federate rooms with more than ${process.env.FEDERATED_DOMAINS_LENGTH || 10} domains`); - } - - // Ensure a genesis event for this room - const genesisEvent = await FederationRoomEvents.createGenesisEvent(getFederationDomain(), normalizedRoom); - - // Dispatch the events - await dispatchEvents(normalizedRoom.federation.domains, [genesisEvent, ...addUserEvents]); -} - -async function afterCreateRoom(roomOwner, room) { - // If the room is federated, ignore - if (room.federation) { - return roomOwner; - } - - // Find all subscriptions of this room - let subscriptions = await Subscriptions.findByRoomIdWhenUsernameExists(room._id).toArray(); - subscriptions = subscriptions.reduce((acc, s) => { - acc[s.u._id] = s; - - return acc; - }, {}); - - // Get all user ids - const userIds = Object.keys(subscriptions); - - // Load all the users - const users = await Users.findUsersWithUsernameByIds(userIds).toArray(); - - // Check if there is a federated user on this room - const hasFederatedUser = users.find((u) => u.username.indexOf('@') !== -1); - - // If there are not federated users on this room, ignore it - if (!hasFederatedUser) { - return roomOwner; - } - - try { - // If the room is not on the allowed types, ignore - if (!checkRoomType(room)) { - throw new Error('Channels cannot be federated'); - } - - clientLogger.debug({ msg: 'afterCreateRoom', roomOwner, room }); - - await doAfterCreateRoom(room, users, subscriptions); - } catch (err) { - await deleteRoom(room._id); - - clientLogger.error({ msg: 'afterCreateRoom => Could not create federated room:', err }); - } - - return room; -} - -export const definition = { - hook: 'afterCreateRoom', - callback: afterCreateRoom, - id: 'federation-after-create-room', -}; diff --git a/apps/meteor/app/federation/server/hooks/afterDeleteMessage.js b/apps/meteor/app/federation/server/hooks/afterDeleteMessage.js deleted file mode 100644 index 4d2928c2ae31a..0000000000000 --- a/apps/meteor/app/federation/server/hooks/afterDeleteMessage.js +++ /dev/null @@ -1,31 +0,0 @@ -import { FederationRoomEvents, Rooms } from '@rocket.chat/models'; - -import { hasExternalDomain } from '../functions/helpers'; -import { dispatchEvent } from '../handler'; -import { getFederationDomain } from '../lib/getFederationDomain'; -import { clientLogger } from '../lib/logger'; - -async function afterDeleteMessage(message) { - const room = await Rooms.findOneById(message.rid, { projection: { federation: 1 } }); - - // If there are not federated users on this room, ignore it - if (!hasExternalDomain(room)) { - return message; - } - - clientLogger.debug({ msg: 'afterDeleteMessage', message, room }); - - // Create the delete message event - const event = await FederationRoomEvents.createDeleteMessageEvent(getFederationDomain(), room._id, message._id); - - // Dispatch event (async) - dispatchEvent(room.federation.domains, event); - - return message; -} - -export const definition = { - hook: 'afterDeleteMessage', - callback: afterDeleteMessage, - id: 'federation-after-delete-message', -}; diff --git a/apps/meteor/app/federation/server/hooks/afterLeaveRoom.js b/apps/meteor/app/federation/server/hooks/afterLeaveRoom.js deleted file mode 100644 index a68db50ab8508..0000000000000 --- a/apps/meteor/app/federation/server/hooks/afterLeaveRoom.js +++ /dev/null @@ -1,54 +0,0 @@ -import { FederationRoomEvents } from '@rocket.chat/models'; - -import { getFederatedRoomData, hasExternalDomain, isLocalUser } from '../functions/helpers'; -import { dispatchEvent } from '../handler'; -import { getFederationDomain } from '../lib/getFederationDomain'; -import { clientLogger } from '../lib/logger'; -import { normalizers } from '../normalizers'; - -async function afterLeaveRoom(user, room) { - const localDomain = getFederationDomain(); - - // If there are not federated users on this room, ignore it - if (!hasExternalDomain(room) && isLocalUser(user, localDomain)) { - return user; - } - - clientLogger.debug({ msg: 'afterLeaveRoom', user, room }); - - const { users } = await getFederatedRoomData(room); - - try { - // Get the domains after leave - const domainsAfterLeave = [...new Set(users.map((u) => u.federation.origin))]; - - // - // Normalize the room's federation status - // - const usersBeforeLeave = users; - usersBeforeLeave.push(user); - - // Get the users domains - const domainsBeforeLeft = [...new Set(usersBeforeLeave.map((u) => u.federation.origin))]; - - // - // Create the user left event - // - const normalizedSourceUser = await normalizers.normalizeUser(user); - - const userLeftEvent = await FederationRoomEvents.createUserLeftEvent(localDomain, room._id, normalizedSourceUser, domainsAfterLeave); - - // Dispatch the events - dispatchEvent(domainsBeforeLeft, userLeftEvent); - } catch (err) { - clientLogger.error({ msg: 'afterLeaveRoom => Could not make user leave:', err }); - } - - return user; -} - -export const definition = { - hook: 'afterLeaveRoom', - callback: afterLeaveRoom, - id: 'federation-after-leave-room', -}; diff --git a/apps/meteor/app/federation/server/hooks/afterMuteUser.js b/apps/meteor/app/federation/server/hooks/afterMuteUser.js deleted file mode 100644 index 5221ccc647272..0000000000000 --- a/apps/meteor/app/federation/server/hooks/afterMuteUser.js +++ /dev/null @@ -1,32 +0,0 @@ -import { FederationRoomEvents } from '@rocket.chat/models'; - -import { hasExternalDomain } from '../functions/helpers'; -import { dispatchEvent } from '../handler'; -import { getFederationDomain } from '../lib/getFederationDomain'; -import { clientLogger } from '../lib/logger'; -import { normalizers } from '../normalizers'; - -async function afterMuteUser(involvedUsers, room) { - // If there are not federated users on this room, ignore it - if (!hasExternalDomain(room)) { - return involvedUsers; - } - - clientLogger.debug({ msg: 'afterMuteUser', involvedUsers, room }); - - const { mutedUser } = involvedUsers; - - // Create the mute user event - const event = await FederationRoomEvents.createMuteUserEvent(getFederationDomain(), room._id, await normalizers.normalizeUser(mutedUser)); - - // Dispatch event (async) - dispatchEvent(room.federation.domains, event); - - return involvedUsers; -} - -export const definition = { - hook: 'afterMuteUser', - callback: afterMuteUser, - id: 'federation-after-mute-user', -}; diff --git a/apps/meteor/app/federation/server/hooks/afterRemoveFromRoom.js b/apps/meteor/app/federation/server/hooks/afterRemoveFromRoom.js deleted file mode 100644 index b2685dd8ac88e..0000000000000 --- a/apps/meteor/app/federation/server/hooks/afterRemoveFromRoom.js +++ /dev/null @@ -1,61 +0,0 @@ -import { FederationRoomEvents } from '@rocket.chat/models'; - -import { getFederatedRoomData, hasExternalDomain, isLocalUser } from '../functions/helpers'; -import { dispatchEvent } from '../handler'; -import { getFederationDomain } from '../lib/getFederationDomain'; -import { clientLogger } from '../lib/logger'; -import { normalizers } from '../normalizers'; - -async function afterRemoveFromRoom(involvedUsers, room) { - const { removedUser } = involvedUsers; - - const localDomain = getFederationDomain(); - - // If there are not federated users on this room, ignore it - if (!hasExternalDomain(room) && isLocalUser(removedUser, localDomain)) { - return involvedUsers; - } - - clientLogger.debug({ msg: 'afterRemoveFromRoom', involvedUsers, room }); - - const { users } = await getFederatedRoomData(room); - - try { - // Get the domains after removal - const domainsAfterRemoval = [...new Set(users.map((u) => u.federation.origin))]; - - // - // Normalize the room's federation status - // - const usersBeforeRemoval = users; - usersBeforeRemoval.push(removedUser); - - // Get the users domains - const domainsBeforeRemoval = [...new Set(usersBeforeRemoval.map((u) => u.federation.origin))]; - - // - // Create the user remove event - // - const normalizedSourceUser = await normalizers.normalizeUser(removedUser); - - const removeUserEvent = await FederationRoomEvents.createRemoveUserEvent( - localDomain, - room._id, - normalizedSourceUser, - domainsAfterRemoval, - ); - - // Dispatch the events - dispatchEvent(domainsBeforeRemoval, removeUserEvent); - } catch (err) { - clientLogger.error({ msg: 'afterRemoveFromRoom => Could not remove user:', err }); - } - - return involvedUsers; -} - -export const definition = { - hook: 'afterRemoveFromRoom', - callback: afterRemoveFromRoom, - id: 'federation-after-remove-from-room', -}; diff --git a/apps/meteor/app/federation/server/hooks/afterSaveMessage.js b/apps/meteor/app/federation/server/hooks/afterSaveMessage.js deleted file mode 100644 index 20c64f87dda8d..0000000000000 --- a/apps/meteor/app/federation/server/hooks/afterSaveMessage.js +++ /dev/null @@ -1,38 +0,0 @@ -import { FederationRoomEvents } from '@rocket.chat/models'; - -import { hasExternalDomain } from '../functions/helpers'; -import { dispatchEvent } from '../handler'; -import { getFederationDomain } from '../lib/getFederationDomain'; -import { clientLogger } from '../lib/logger'; -import { normalizers } from '../normalizers'; - -async function afterSaveMessage(message, { room }) { - // If there are not federated users on this room, ignore it - if (!hasExternalDomain(room)) { - return message; - } - - clientLogger.debug({ msg: 'afterSaveMessage', message, room }); - - let event; - - // If editedAt exists, it means it is an update - if (message.editedAt) { - // Create the edit message event - event = await FederationRoomEvents.createEditMessageEvent(getFederationDomain(), room._id, normalizers.normalizeMessage(message)); - } else { - // Create the message event - event = await FederationRoomEvents.createMessageEvent(getFederationDomain(), room._id, normalizers.normalizeMessage(message)); - } - - // Dispatch event (async) - dispatchEvent(room.federation.domains, event); - - return message; -} - -export const definition = { - hook: 'afterSaveMessage', - callback: afterSaveMessage, - id: 'federation-after-save-message', -}; diff --git a/apps/meteor/app/federation/server/hooks/afterSetReaction.js b/apps/meteor/app/federation/server/hooks/afterSetReaction.js deleted file mode 100644 index 485080dbd7062..0000000000000 --- a/apps/meteor/app/federation/server/hooks/afterSetReaction.js +++ /dev/null @@ -1,37 +0,0 @@ -import { FederationRoomEvents, Rooms } from '@rocket.chat/models'; - -import { hasExternalDomain } from '../functions/helpers'; -import { dispatchEvent } from '../handler'; -import { getFederationDomain } from '../lib/getFederationDomain'; -import { clientLogger } from '../lib/logger'; - -async function afterSetReaction(message, { user, reaction }) { - const room = await Rooms.findOneById(message.rid, { projection: { federation: 1 } }); - - // If there are not federated users on this room, ignore it - if (!hasExternalDomain(room)) { - return message; - } - - clientLogger.debug({ msg: 'afterSetReaction', message, room, user, reaction }); - - // Create the event - const event = await FederationRoomEvents.createSetMessageReactionEvent( - getFederationDomain(), - room._id, - message._id, - user.username, - reaction, - ); - - // Dispatch event (async) - dispatchEvent(room.federation.domains, event); - - return message; -} - -export const definition = { - hook: 'afterSetReaction', - callback: afterSetReaction, - id: 'federation-after-set-reaction', -}; diff --git a/apps/meteor/app/federation/server/hooks/afterUnmuteUser.js b/apps/meteor/app/federation/server/hooks/afterUnmuteUser.js deleted file mode 100644 index 57cfcee596011..0000000000000 --- a/apps/meteor/app/federation/server/hooks/afterUnmuteUser.js +++ /dev/null @@ -1,36 +0,0 @@ -import { FederationRoomEvents } from '@rocket.chat/models'; - -import { hasExternalDomain } from '../functions/helpers'; -import { dispatchEvent } from '../handler'; -import { getFederationDomain } from '../lib/getFederationDomain'; -import { clientLogger } from '../lib/logger'; -import { normalizers } from '../normalizers'; - -async function afterUnmuteUser(involvedUsers, room) { - // If there are not federated users on this room, ignore it - if (!hasExternalDomain(room)) { - return involvedUsers; - } - - clientLogger.debug({ msg: 'afterUnmuteUser', involvedUsers, room }); - - const { unmutedUser } = involvedUsers; - - // Create the mute user event - const event = await FederationRoomEvents.createUnmuteUserEvent( - getFederationDomain(), - room._id, - await normalizers.normalizeUser(unmutedUser), - ); - - // Dispatch event (async) - dispatchEvent(room.federation.domains, event); - - return involvedUsers; -} - -export const definition = { - hook: 'afterUnmuteUser', - callback: afterUnmuteUser, - id: 'federation-after-unmute-user', -}; diff --git a/apps/meteor/app/federation/server/hooks/afterUnsetReaction.js b/apps/meteor/app/federation/server/hooks/afterUnsetReaction.js deleted file mode 100644 index 995146b290bf4..0000000000000 --- a/apps/meteor/app/federation/server/hooks/afterUnsetReaction.js +++ /dev/null @@ -1,37 +0,0 @@ -import { FederationRoomEvents, Rooms } from '@rocket.chat/models'; - -import { hasExternalDomain } from '../functions/helpers'; -import { dispatchEvent } from '../handler'; -import { getFederationDomain } from '../lib/getFederationDomain'; -import { clientLogger } from '../lib/logger'; - -async function afterUnsetReaction(message, { user, reaction }) { - const room = await Rooms.findOneById(message.rid, { projection: { federation: 1 } }); - - // If there are not federated users on this room, ignore it - if (!hasExternalDomain(room)) { - return message; - } - - clientLogger.debug({ msg: 'afterUnsetReaction', message, room, user, reaction }); - - // Create the event - const event = await FederationRoomEvents.createUnsetMessageReactionEvent( - getFederationDomain(), - room._id, - message._id, - user.username, - reaction, - ); - - // Dispatch event (async) - dispatchEvent(room.federation.domains, event); - - return message; -} - -export const definition = { - hook: 'afterUnsetReaction', - callback: afterUnsetReaction, - id: 'federation-after-unset-reaction', -}; diff --git a/apps/meteor/app/federation/server/hooks/beforeDeleteRoom.js b/apps/meteor/app/federation/server/hooks/beforeDeleteRoom.js deleted file mode 100644 index c7105b4d177bc..0000000000000 --- a/apps/meteor/app/federation/server/hooks/beforeDeleteRoom.js +++ /dev/null @@ -1,42 +0,0 @@ -import { FederationRoomEvents, Rooms } from '@rocket.chat/models'; - -import { hasExternalDomain } from '../functions/helpers'; -import { dispatchEvent } from '../handler'; -import { getFederationDomain } from '../lib/getFederationDomain'; -import { clientLogger } from '../lib/logger'; - -async function beforeDeleteRoom(roomId) { - const room = await Rooms.findOneById(roomId, { projection: { federation: 1 } }); - - // If room does not exist, skip - if (!room) { - return roomId; - } - - // If there are not federated users on this room, ignore it - if (!hasExternalDomain(room)) { - return roomId; - } - - clientLogger.debug({ msg: 'beforeDeleteRoom', room }); - - try { - // Create the message event - const event = await FederationRoomEvents.createDeleteRoomEvent(getFederationDomain(), room._id); - - // Dispatch event (async) - dispatchEvent(room.federation.domains, event); - } catch (err) { - clientLogger.error({ msg: 'beforeDeleteRoom => Could not remove room:', err }); - - throw err; - } - - return roomId; -} - -export const definition = { - hook: 'beforeDeleteRoom', - callback: beforeDeleteRoom, - id: 'federation-before-delete-room', -}; diff --git a/apps/meteor/app/federation/server/index.ts b/apps/meteor/app/federation/server/index.ts deleted file mode 100644 index 0fddfc2fe2eb3..0000000000000 --- a/apps/meteor/app/federation/server/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import './methods'; -import './endpoints'; -import './startup'; diff --git a/apps/meteor/app/federation/server/lib/callbacks.ts b/apps/meteor/app/federation/server/lib/callbacks.ts deleted file mode 100644 index c379a50b96a22..0000000000000 --- a/apps/meteor/app/federation/server/lib/callbacks.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { callbacks } from '../../../../lib/callbacks'; -import type { Hook } from '../../../../lib/callbacks'; -import { settings } from '../../../settings/server'; - -type CallbackDefinition = { - hook: Hook; - callback: (...args: any[]) => any; - id: string; -}; - -const callbackDefinitions: CallbackDefinition[] = []; - -function enableCallback(definition: CallbackDefinition): void { - callbacks.add(definition.hook, definition.callback, callbacks.priority.LOW, definition.id); -} - -export function registerCallback(callbackDefinition: CallbackDefinition) { - callbackDefinitions.push(callbackDefinition); - - if (settings.get('FEDERATION_Enabled')) { - enableCallback(callbackDefinition); - } -} - -export function enableCallbacks(): void { - for (const definition of callbackDefinitions) { - enableCallback(definition); - } -} - -export function disableCallbacks(): void { - for (const definition of callbackDefinitions) { - callbacks.remove(definition.hook, definition.id); - } -} diff --git a/apps/meteor/app/federation/server/lib/context.ts b/apps/meteor/app/federation/server/lib/context.ts deleted file mode 100644 index e62e5a19a43ce..0000000000000 --- a/apps/meteor/app/federation/server/lib/context.ts +++ /dev/null @@ -1,19 +0,0 @@ -export const contextDefinitions = { - ROOM: { - type: 'room' as const, - isRoom(event: { context: { roomId?: string } }): boolean { - return !!event.context.roomId; - }, - contextQuery(roomId: string) { - return { roomId }; - }, - }, - - defineType(event: { context: { roomId?: string } }): 'room' | 'undefined' { - if (this.ROOM.isRoom(event)) { - return this.ROOM.type; - } - - return 'undefined'; - }, -}; diff --git a/apps/meteor/app/federation/server/lib/crypt.js b/apps/meteor/app/federation/server/lib/crypt.js deleted file mode 100644 index 786f15416ec50..0000000000000 --- a/apps/meteor/app/federation/server/lib/crypt.js +++ /dev/null @@ -1,68 +0,0 @@ -import { FederationKeys } from '@rocket.chat/models'; - -import { search } from './dns'; -import { getFederationDomain } from './getFederationDomain'; -import { cryptLogger } from './logger'; - -async function decrypt(data, peerKey) { - // - // Decrypt the payload - const payloadBuffer = Buffer.from(data); - - // Decrypt with the peer's public key - try { - data = (await FederationKeys.loadKey(peerKey, 'public')).decryptPublic(payloadBuffer); - - // Decrypt with the local private key - data = (await FederationKeys.getPrivateKey()).decrypt(data); - } catch (err) { - cryptLogger.error(err); - - throw new Error('Could not decrypt'); - } - - return JSON.parse(data.toString()); -} - -export async function decryptIfNeeded(request, bodyParams) { - // - // Look for the domain that sent this event - const remotePeerDomain = request.headers['x-federation-domain']; - - if (!remotePeerDomain) { - throw new Error('Domain is unknown, ignoring event'); - } - - // - // Decrypt payload if needed - if (remotePeerDomain === getFederationDomain()) { - return bodyParams; - } - // - // Find the peer's public key - const { publicKey: peerKey } = await search(remotePeerDomain); - - if (!peerKey) { - throw new Error("Could not find the peer's public key to decrypt"); - } - - return decrypt(bodyParams, peerKey); -} - -export async function encrypt(data, peerKey) { - if (!data) { - return data; - } - - try { - // Encrypt with the peer's public key - data = (await FederationKeys.loadKey(peerKey, 'public')).encrypt(data); - - // Encrypt with the local private key - return (await FederationKeys.getPrivateKey()).encryptPrivate(data); - } catch (err) { - cryptLogger.error(err); - - throw new Error('Could not encrypt'); - } -} diff --git a/apps/meteor/app/federation/server/lib/dns.js b/apps/meteor/app/federation/server/lib/dns.js deleted file mode 100644 index 83ce0cc70349f..0000000000000 --- a/apps/meteor/app/federation/server/lib/dns.js +++ /dev/null @@ -1,155 +0,0 @@ -import dnsResolver from 'dns'; -import util from 'util'; - -import mem from 'mem'; - -import { federationRequest } from './http'; -import { isFederationEnabled } from './isFederationEnabled'; -import { dnsLogger } from './logger'; -import * as federationErrors from '../functions/errors'; - -const dnsResolveSRV = util.promisify(dnsResolver.resolveSrv); -const dnsResolveTXT = util.promisify(dnsResolver.resolveTxt); - -const cacheMaxAge = 3600000; // one hour -const memoizedDnsResolveSRV = mem(dnsResolveSRV, { maxAge: cacheMaxAge }); -const memoizedDnsResolveTXT = mem(dnsResolveTXT, { maxAge: cacheMaxAge }); - -const hubUrl = process.env.NODE_ENV === 'development' ? 'http://localhost:8080' : 'https://hub.rocket.chat'; - -export async function registerWithHub(peerDomain, url, publicKey) { - const body = { domain: peerDomain, url, public_key: publicKey }; - - try { - // If there is no DNS entry for that, get from the Hub - await federationRequest('POST', `${hubUrl}/api/v1/peers`, body); - - return true; - } catch (err) { - dnsLogger.error(err); - - throw federationErrors.peerCouldNotBeRegisteredWithHub('dns.registerWithHub'); - } -} - -async function searchHub(peerDomain) { - try { - dnsLogger.debug(`searchHub: peerDomain=${peerDomain}`); - - // If there is no DNS entry for that, get from the Hub - const { - data: { peer }, - } = await federationRequest('GET', `${hubUrl}/api/v1/peers?search=${peerDomain}`); - - if (!peer) { - dnsLogger.debug(`searchHub: could not find peerDomain=${peerDomain}`); - throw federationErrors.peerCouldNotBeRegisteredWithHub('dns.registerWithHub'); - } - - const { url, public_key: publicKey } = peer; - - dnsLogger.debug(`searchHub: found peerDomain=${peerDomain} url=${url}`); - - return { - url, - peerDomain, - publicKey, - }; - } catch (err) { - dnsLogger.error(err); - - throw federationErrors.peerNotFoundUsingDNS('dns.searchHub'); - } -} - -export async function search(peerDomain) { - if (!isFederationEnabled()) { - throw federationErrors.disabled('dns.search'); - } - - dnsLogger.debug(`search: peerDomain=${peerDomain}`); - - let srvEntries = []; - let protocol = ''; - - // Search by HTTPS first - try { - dnsLogger.debug(`search: peerDomain=${peerDomain} srv=_rocketchat._https.${peerDomain}`); - srvEntries = await memoizedDnsResolveSRV(`_rocketchat._https.${peerDomain}`); - protocol = 'https'; - } catch (err) { - // Ignore errors when looking for DNS entries - } - - // If there is not entry, try with http - if (!srvEntries.length) { - try { - dnsLogger.debug(`search: peerDomain=${peerDomain} srv=_rocketchat._http.${peerDomain}`); - srvEntries = await memoizedDnsResolveSRV(`_rocketchat._http.${peerDomain}`); - protocol = 'http'; - } catch (err) { - // Ignore errors when looking for DNS entries - } - } - - // If there is not entry, try with tcp - if (!srvEntries.length) { - try { - dnsLogger.debug(`search: peerDomain=${peerDomain} srv=_rocketchat._tcp.${peerDomain}`); - srvEntries = await memoizedDnsResolveSRV(`_rocketchat._tcp.${peerDomain}`); - protocol = 'https'; // https is the default - - // Then, also try to get the protocol - dnsLogger.debug(`search: peerDomain=${peerDomain} txt=rocketchat-tcp-protocol.${peerDomain}`); - protocol = await memoizedDnsResolveSRV(`rocketchat-tcp-protocol.${peerDomain}`); - protocol = protocol[0].join(''); - - if (protocol !== 'http' && protocol !== 'https') { - protocol = null; - } - } catch (err) { - // if there is an error while getting the _tcp entry, it means the config is not there - // but if there is an error looking for the `_rocketchat_tcp_protocol` entry, it means we should use https - } - } - - const [srvEntry] = srvEntries; - - // If there is no entry, throw error - if (!srvEntry || !protocol) { - dnsLogger.debug({ - msg: 'search: could not find valid SRV entry', - peerDomain, - srvEntry, - protocol, - }); - return searchHub(peerDomain); - } - - let publicKey = null; - - // Get the public key from the TXT record - try { - dnsLogger.debug(`search: peerDomain=${peerDomain} txt=rocketchat-public-key.${peerDomain}`); - const publicKeyTxtRecords = await memoizedDnsResolveTXT(`rocketchat-public-key.${peerDomain}`); - - // Join the TXT record, that might be split - publicKey = publicKeyTxtRecords[0].join(''); - } catch (err) { - // Ignore errors when looking for DNS entries - } - - // If there is no entry, throw error - if (!publicKey) { - dnsLogger.debug(`search: could not find TXT entry for peerDomain=${peerDomain} - SRV entry found`); - return searchHub(peerDomain); - } - - dnsLogger.debug({ msg: 'search: found', peerDomain, srvEntry, protocol }); - - return { - url: `${protocol}://${srvEntry.name}:${srvEntry.port}`, - peerDomain, - publicKey, - }; -} diff --git a/apps/meteor/app/federation/server/lib/getFederationDiscoveryMethod.ts b/apps/meteor/app/federation/server/lib/getFederationDiscoveryMethod.ts deleted file mode 100644 index b8ea8c4f6ce6e..0000000000000 --- a/apps/meteor/app/federation/server/lib/getFederationDiscoveryMethod.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { settings } from '../../../settings/server'; - -export const getFederationDiscoveryMethod = () => settings.get('FEDERATION_Discovery_Method'); diff --git a/apps/meteor/app/federation/server/lib/getFederationDomain.ts b/apps/meteor/app/federation/server/lib/getFederationDomain.ts deleted file mode 100644 index 80f683743f2d7..0000000000000 --- a/apps/meteor/app/federation/server/lib/getFederationDomain.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { settings } from '../../../settings/server'; - -export const getFederationDomain = () => settings.get('FEDERATION_Domain').replace('@', ''); diff --git a/apps/meteor/app/federation/server/lib/http.js b/apps/meteor/app/federation/server/lib/http.js deleted file mode 100644 index d663d93d2f156..0000000000000 --- a/apps/meteor/app/federation/server/lib/http.js +++ /dev/null @@ -1,60 +0,0 @@ -import { serverFetch as fetch } from '@rocket.chat/server-fetch'; -import EJSON from 'ejson'; - -import { encrypt } from './crypt'; -import { search } from './dns'; -import { getFederationDomain } from './getFederationDomain'; -import { httpLogger } from './logger'; - -export async function federationRequest(method, url, body, headers, peerKey = null) { - let data = null; - - if ((method === 'POST' || method === 'PUT') && body) { - data = EJSON.toJSONValue(body); - - if (peerKey) { - data = await encrypt(data, peerKey); - } - } - - httpLogger.debug(`[${method}] ${url}`); - - const request = await fetch(url, { - method, - headers: { ...headers, 'x-federation-domain': getFederationDomain() }, - body: data, - timeout: 2000, - }); - return request.json(); -} - -export async function federationRequestToPeer(method, peerDomain, uri, body, options = {}) { - const ignoreErrors = peerDomain === getFederationDomain() ? false : options.ignoreErrors; - - const { url: baseUrl, publicKey } = search(peerDomain); - - let peerKey = null; - - // Only encrypt if it is not local - if (peerDomain !== getFederationDomain()) { - peerKey = publicKey; - } - - let result; - - try { - httpLogger.debug({ msg: 'federationRequestToPeer', url: `${baseUrl}${uri}` }); - - result = await federationRequest(method, `${baseUrl}${uri}`, body, options.headers || {}, peerKey); - } catch (err) { - httpLogger.error({ msg: `${ignoreErrors ? '[IGNORED] ' : ''}Error`, err }); - - if (!ignoreErrors) { - throw err; - } else { - return { success: false }; - } - } - - return { success: true, data: result }; -} diff --git a/apps/meteor/app/federation/server/lib/isFederationEnabled.ts b/apps/meteor/app/federation/server/lib/isFederationEnabled.ts deleted file mode 100644 index e3edb818e602d..0000000000000 --- a/apps/meteor/app/federation/server/lib/isFederationEnabled.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { settings } from '../../../settings/server'; - -export const isFederationEnabled = () => settings.get('FEDERATION_Enabled'); diff --git a/apps/meteor/app/federation/server/lib/logger.ts b/apps/meteor/app/federation/server/lib/logger.ts deleted file mode 100644 index e8cf0ae12844a..0000000000000 --- a/apps/meteor/app/federation/server/lib/logger.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Logger } from '@rocket.chat/logger'; - -const logger = new Logger('Federation'); - -export const clientLogger = logger.section('client'); -export const cryptLogger = logger.section('crypt'); -export const dnsLogger = logger.section('dns'); -export const httpLogger = logger.section('http'); -export const serverLogger = logger.section('server'); -export const setupLogger = logger.section('Setup'); diff --git a/apps/meteor/app/federation/server/methods/dashboard.ts b/apps/meteor/app/federation/server/methods/dashboard.ts deleted file mode 100644 index eacb42b24cbb2..0000000000000 --- a/apps/meteor/app/federation/server/methods/dashboard.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { IFederationServer } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { federationGetServers, federationGetOverviewData } from '../functions/dashboard'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'federation:getServers': () => { data: IFederationServer[] }; - 'federation:getOverviewData': () => { data: { title: string; value: number }[] }; - } -} - -Meteor.methods({ - 'federation:getServers': federationGetServers, - 'federation:getOverviewData': federationGetOverviewData, -}); diff --git a/apps/meteor/app/federation/server/methods/index.ts b/apps/meteor/app/federation/server/methods/index.ts deleted file mode 100644 index 26d3e1f619ff8..0000000000000 --- a/apps/meteor/app/federation/server/methods/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import './dashboard'; -import './loadContextEvents'; -import './testSetup'; diff --git a/apps/meteor/app/federation/server/methods/loadContextEvents.ts b/apps/meteor/app/federation/server/methods/loadContextEvents.ts deleted file mode 100644 index e20acb7e37c99..0000000000000 --- a/apps/meteor/app/federation/server/methods/loadContextEvents.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { IFederationEvent } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { FederationRoomEvents } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'federation:loadContextEvents'(latestEventTimestamp: number): IFederationEvent[]; - } -} - -Meteor.methods({ - 'federation:loadContextEvents': async (latestEventTimestamp) => { - const uid = Meteor.userId(); - if (!uid) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'loadContextEvents' }); - } - - if (!(await hasPermissionAsync(uid, 'view-federation-data'))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { - method: 'loadContextEvents', - }); - } - - return FederationRoomEvents.find({ timestamp: { $gt: new Date(latestEventTimestamp) } }, { sort: { timestamp: 1 } }).toArray(); - }, -}); diff --git a/apps/meteor/app/federation/server/methods/testSetup.ts b/apps/meteor/app/federation/server/methods/testSetup.ts deleted file mode 100644 index bc50ef1581944..0000000000000 --- a/apps/meteor/app/federation/server/methods/testSetup.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { eventTypes } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { dispatchEvent } from '../handler'; -import { getFederationDomain } from '../lib/getFederationDomain'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - FEDERATION_Test_Setup(): { message: string }; - } -} - -Meteor.methods({ - FEDERATION_Test_Setup() { - try { - void dispatchEvent([getFederationDomain()], { - type: eventTypes.PING, - }); - - return { - message: 'FEDERATION_Test_Setup_Success', - }; - } catch (err) { - throw new Meteor.Error('FEDERATION_Test_Setup_Error'); - } - }, -}); diff --git a/apps/meteor/app/federation/server/normalizers/index.ts b/apps/meteor/app/federation/server/normalizers/index.ts deleted file mode 100644 index ae1e2183626b6..0000000000000 --- a/apps/meteor/app/federation/server/normalizers/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import message from './message'; -import room from './room'; -import subscription from './subscription'; -import user from './user'; - -export const normalizers = { - ...message, - ...room, - ...subscription, - ...user, -}; diff --git a/apps/meteor/app/federation/server/normalizers/message.js b/apps/meteor/app/federation/server/normalizers/message.js deleted file mode 100644 index 491db6a06c8eb..0000000000000 --- a/apps/meteor/app/federation/server/normalizers/message.js +++ /dev/null @@ -1,102 +0,0 @@ -import { getNameAndDomain, isFullyQualified } from '../functions/helpers'; -import { getFederationDomain } from '../lib/getFederationDomain'; - -const denormalizeMessage = (originalResource) => { - const resource = { ...originalResource }; - - const [username, domain] = getNameAndDomain(resource.u.username); - - const localDomain = getFederationDomain(); - - // Denormalize username - resource.u.username = domain === localDomain ? username : resource.u.username; - - // Denormalize mentions - for (const mention of resource.mentions) { - // Ignore if we are dealing with all, here or rocket.cat - if (['all', 'here', 'rocket.cat'].indexOf(mention.username) !== -1) { - continue; - } - - const [username, domain] = getNameAndDomain(mention.username); - - if (domain === localDomain) { - const originalUsername = mention.username; - - mention.username = username; - - resource.msg = resource.msg.split(originalUsername).join(username); - } - } - - // Denormalize channels - for (const channel of resource.channels) { - // Ignore if we are dealing with all, here or rocket.cat - if (['all', 'here', 'rocket.cat'].indexOf(channel.name) !== -1) { - continue; - } - - const [username, domain] = getNameAndDomain(channel.name); - - if (domain === localDomain) { - const originalUsername = channel.name; - - channel.name = username; - - resource.msg = resource.msg.split(originalUsername).join(username); - } - } - - return resource; -}; - -const denormalizeAllMessages = (resources) => resources.map(denormalizeMessage); - -const normalizeMessage = (originalResource) => { - const resource = { ...originalResource }; - - resource.u.username = !isFullyQualified(resource.u.username) ? `${resource.u.username}@${getFederationDomain()}` : resource.u.username; - - // Federation - resource.federation = resource.federation || { - origin: getFederationDomain(), // The origin of this resource, where it was created - }; - - // Normalize mentions - for (const mention of resource.mentions || []) { - // Ignore if we are dealing with all, here or rocket.cat - if (['all', 'here', 'rocket.cat'].indexOf(mention.username) !== -1) { - continue; - } - - if (!isFullyQualified(mention.username)) { - const originalUsername = mention.username; - - mention.username = `${mention.username}@${getFederationDomain()}`; - - resource.msg = resource.msg.split(originalUsername).join(mention.username); - } - } - - // Normalize channels - for (const channel of resource.channels || []) { - if (!isFullyQualified(channel.name)) { - const originalUsername = channel.name; - - channel.name = `${channel.name}@${getFederationDomain()}`; - - resource.msg = resource.msg.split(originalUsername).join(channel.name); - } - } - - return resource; -}; - -const normalizeAllMessages = (resources) => resources.map(normalizeMessage); - -export default { - denormalizeMessage, - denormalizeAllMessages, - normalizeMessage, - normalizeAllMessages, -}; diff --git a/apps/meteor/app/federation/server/normalizers/room.js b/apps/meteor/app/federation/server/normalizers/room.js deleted file mode 100644 index 2b92279aaa6ae..0000000000000 --- a/apps/meteor/app/federation/server/normalizers/room.js +++ /dev/null @@ -1,95 +0,0 @@ -import { getNameAndDomain, isFullyQualified } from '../functions/helpers'; -import { getFederationDomain } from '../lib/getFederationDomain'; - -const denormalizeRoom = (originalResource) => { - const resource = { ...originalResource }; - - if (resource.t === 'd') { - resource.usernames = resource.usernames.map((u) => { - const [username, domain] = getNameAndDomain(u); - - return domain === getFederationDomain() ? username : u; - }); - } else { - // Denormalize room name - const [roomName, roomDomain] = getNameAndDomain(resource.name); - - resource.name = roomDomain === getFederationDomain() ? roomName : resource.name; - - // Denormalize room owner name - const [username, userDomain] = getNameAndDomain(resource.u.username); - - resource.u.username = userDomain === getFederationDomain() ? username : resource.u.username; - - // Denormalize muted users - if (resource.muted) { - resource.muted = resource.muted.map((u) => { - const [username, domain] = getNameAndDomain(u); - - return domain === getFederationDomain() ? username : u; - }); - } - - // Denormalize unmuted users - if (resource.unmuted) { - resource.unmuted = resource.unmuted.map((u) => { - const [username, domain] = getNameAndDomain(u); - - return domain === getFederationDomain() ? username : u; - }); - } - } - - return resource; -}; - -const normalizeRoom = (originalResource, users) => { - const resource = { ...originalResource }; - - let domains = ''; - - if (resource.t === 'd') { - // Handle user names, adding the Federation domain to local users - resource.usernames = resource.usernames.map((u) => (!isFullyQualified(u) ? `${u}@${getFederationDomain()}` : u)); - - // Get the domains of the usernames - domains = resource.usernames.map((u) => getNameAndDomain(u)[1]); - } else { - // Ensure private - resource.t = 'p'; - - // Normalize room name - resource.name = !isFullyQualified(resource.name) ? `${resource.name}@${getFederationDomain()}` : resource.name; - - // Get the users domains - domains = users.map((u) => u.federation.origin); - - // Normalize the username - resource.u.username = !isFullyQualified(resource.u.username) ? `${resource.u.username}@${getFederationDomain()}` : resource.u.username; - - // Normalize the muted users - if (resource.muted) { - resource.muted = resource.muted.map((u) => (!isFullyQualified(u) ? `${u}@${getFederationDomain()}` : u)); - } - - // Normalize the unmuted users - if (resource.unmuted) { - resource.unmuted = resource.unmuted.map((u) => (!isFullyQualified(u) ? `${u}@${getFederationDomain()}` : u)); - } - } - - domains = [...new Set(domains)]; - - // Federation - resource.federation = resource.federation || { - origin: getFederationDomain(), // The origin of this resource, where it was created - domains, // The domains where this room exist (or will exist) - }; - - return resource; -}; - -export default { - denormalizeRoom, - normalizeRoom, -}; diff --git a/apps/meteor/app/federation/server/normalizers/subscription.js b/apps/meteor/app/federation/server/normalizers/subscription.js deleted file mode 100644 index 7cd3311077fe1..0000000000000 --- a/apps/meteor/app/federation/server/normalizers/subscription.js +++ /dev/null @@ -1,42 +0,0 @@ -import { getNameAndDomain, isFullyQualified } from '../functions/helpers'; -import { getFederationDomain } from '../lib/getFederationDomain'; - -const denormalizeSubscription = (originalResource) => { - const resource = { ...originalResource }; - - const [username, domain] = getNameAndDomain(resource.u.username); - - resource.u.username = domain === getFederationDomain() ? username : resource.u.username; - - const [nameUsername, nameDomain] = getNameAndDomain(resource.name); - - resource.name = nameDomain === getFederationDomain() ? nameUsername : resource.name; - - return resource; -}; - -const denormalizeAllSubscriptions = (resources) => resources.map(denormalizeSubscription); - -const normalizeSubscription = (originalResource) => { - const resource = { ...originalResource }; - - resource.u.username = !isFullyQualified(resource.u.username) ? `${resource.u.username}@${getFederationDomain()}` : resource.u.username; - - resource.name = !isFullyQualified(resource.name) ? `${resource.name}@${getFederationDomain()}` : resource.name; - - // Federation - resource.federation = resource.federation || { - origin: getFederationDomain(), // The origin of this resource, where it was created - }; - - return resource; -}; - -const normalizeAllSubscriptions = (resources) => resources.map(normalizeSubscription); - -export default { - denormalizeSubscription, - denormalizeAllSubscriptions, - normalizeSubscription, - normalizeAllSubscriptions, -}; diff --git a/apps/meteor/app/federation/server/normalizers/user.js b/apps/meteor/app/federation/server/normalizers/user.js deleted file mode 100644 index a2a55e07aea29..0000000000000 --- a/apps/meteor/app/federation/server/normalizers/user.js +++ /dev/null @@ -1,78 +0,0 @@ -import { Users } from '@rocket.chat/models'; -import _ from 'underscore'; - -import { getNameAndDomain, isFullyQualified } from '../functions/helpers'; -import { getFederationDomain } from '../lib/getFederationDomain'; - -const denormalizeUser = (originalResource) => { - const resource = { ...originalResource }; - - // Only denormalize local emails - if (resource.federation && resource.federation.origin === getFederationDomain()) { - resource.emails = [ - { - address: resource.federation.originalInfo.email, - }, - ]; - } - - const [username, domain] = getNameAndDomain(resource.username); - - resource.username = domain === getFederationDomain() ? username : resource.username; - - return resource; -}; - -const denormalizeAllUsers = (resources) => resources.map(denormalizeUser); - -const normalizeUser = async (originalResource) => { - // Get only what we need, non-sensitive data - const resource = _.pick( - originalResource, - '_id', - 'username', - 'type', - 'emails', - 'name', - 'federation', - 'isRemote', - 'createdAt', - '_updatedAt', - ); - - resource.emails = [ - { - address: `${resource._id}@${getFederationDomain()}`, - }, - ]; - const email = resource.emails[0].address; - - resource.active = true; - resource.roles = ['user']; - resource.status = 'online'; - resource.username = !isFullyQualified(resource.username) ? `${resource.username}@${getFederationDomain()}` : resource.username; - - // Federation - resource.federation = resource.federation || { - origin: getFederationDomain(), - originalInfo: { - email, - }, - }; - - resource.isRemote = resource.federation.origin !== getFederationDomain(); - - // Persist the normalization - await Users.updateOne({ _id: resource._id }, { $set: { isRemote: resource.isRemote, federation: resource.federation } }); - - return resource; -}; - -const normalizeAllUsers = (resources) => Promise.all(resources.map(normalizeUser)); - -export default { - denormalizeUser, - denormalizeAllUsers, - normalizeUser, - normalizeAllUsers, -}; diff --git a/apps/meteor/app/federation/server/startup/index.ts b/apps/meteor/app/federation/server/startup/index.ts deleted file mode 100644 index 5d9191525d1b9..0000000000000 --- a/apps/meteor/app/federation/server/startup/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './generateKeys'; -import './registerCallbacks'; diff --git a/apps/meteor/app/federation/server/startup/registerCallbacks.js b/apps/meteor/app/federation/server/startup/registerCallbacks.js deleted file mode 100644 index 54f5ee443450b..0000000000000 --- a/apps/meteor/app/federation/server/startup/registerCallbacks.js +++ /dev/null @@ -1,26 +0,0 @@ -import { definition as afterAddedToRoomDef } from '../hooks/afterAddedToRoom'; -import { definition as afterCreateDirectRoomDef } from '../hooks/afterCreateDirectRoom'; -import { definition as afterCreateRoomDef } from '../hooks/afterCreateRoom'; -import { definition as afterDeleteMessageDef } from '../hooks/afterDeleteMessage'; -import { definition as afterLeaveRoomDef } from '../hooks/afterLeaveRoom'; -import { definition as afterMuteUserDef } from '../hooks/afterMuteUser'; -import { definition as afterRemoveFromRoomDef } from '../hooks/afterRemoveFromRoom'; -import { definition as afterSaveMessageDef } from '../hooks/afterSaveMessage'; -import { definition as afterSetReactionDef } from '../hooks/afterSetReaction'; -import { definition as afterUnmuteUserDef } from '../hooks/afterUnmuteUser'; -import { definition as afterUnsetReactionDef } from '../hooks/afterUnsetReaction'; -import { definition as beforeDeleteRoomDef } from '../hooks/beforeDeleteRoom'; -import { registerCallback } from '../lib/callbacks'; - -registerCallback(afterAddedToRoomDef); -registerCallback(afterCreateDirectRoomDef); -registerCallback(afterCreateRoomDef); -registerCallback(afterDeleteMessageDef); -registerCallback(afterLeaveRoomDef); -registerCallback(afterMuteUserDef); -registerCallback(beforeDeleteRoomDef); -registerCallback(afterSaveMessageDef); -registerCallback(afterSetReactionDef); -registerCallback(afterUnmuteUserDef); -registerCallback(afterUnsetReactionDef); -registerCallback(afterRemoveFromRoomDef); diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index c37be6421091d..a68e154dc36ef 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -39,7 +39,6 @@ import { isRunningMs } from '../../../../server/lib/isRunningMs'; import { getControl } from '../../../../server/lib/migrations'; import { getSettingsStatistics } from '../../../../server/lib/statistics/getSettingsStatistics'; import { getMatrixFederationStatistics } from '../../../../server/services/federation/infrastructure/rocket-chat/adapters/Statistics'; -import { getStatistics as federationGetStatistics } from '../../../federation/server/functions/dashboard'; import { settings } from '../../../settings/server'; import { Info } from '../../../utils/rocketchat.info'; import { getMongoInfo } from '../../../utils/server/functions/getMongoInfo'; @@ -354,14 +353,6 @@ export const statistics = { statistics.totalDirectMessages + statistics.totalLivechatMessages; - // Federation statistics - statsPms.push( - federationGetStatistics().then((federationOverviewData) => { - statistics.federatedServers = federationOverviewData.numberOfServers; - statistics.federatedUsers = federationOverviewData.numberOfFederatedUsers; - }), - ); - statistics.lastLogin = (await Users.getLastLogin())?.toString() || ''; statistics.lastMessageSentAt = await Messages.getLastTimestamp(); statistics.lastSeenSubscription = (await Subscriptions.getLastSeen())?.toString() || ''; diff --git a/apps/meteor/client/views/admin/federationDashboard/FederationDashboardPage.stories.tsx b/apps/meteor/client/views/admin/federationDashboard/FederationDashboardPage.stories.tsx deleted file mode 100644 index 1f1ab8040dd44..0000000000000 --- a/apps/meteor/client/views/admin/federationDashboard/FederationDashboardPage.stories.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import type { Meta, StoryFn } from '@storybook/react'; - -import FederationDashboardPage from './FederationDashboardPage'; - -export default { - component: FederationDashboardPage, - parameters: { - layout: 'fullscreen', - controls: { hideNoControlsWarning: true }, - }, -} satisfies Meta; - -const Template: StoryFn = () => ; - -export const Example = Template.bind({}); -Example.parameters = { - serverContext: { - callMethod: { - 'federation:getOverviewData': async () => ({ - data: [ - { - title: 'Number_of_events', - value: 123, - }, - { - title: 'Number_of_federated_users', - value: 123, - }, - { - title: 'Number_of_federated_servers', - value: 123, - }, - ], - }), - 'federation:getServers': async () => ({ - data: [ - { - _id: 'server-id-1', - domain: 'open.rocket.chat', - }, - { - _id: 'server-id-2', - domain: 'unstable.rocket.chat', - }, - ], - }), - }, - }, -}; - -export const Loading = Template.bind({}); -Loading.parameters = { - serverContext: { - callMethod: { - 'federation:getOverviewData': 'infinite', - 'federation:getServers': 'infinite', - }, - }, -}; - -export const Errored = Template.bind({}); -Errored.parameters = { - serverContext: { - callMethod: { - 'federation:getOverviewData': 'errored', - 'federation:getServers': 'errored', - }, - }, -}; diff --git a/apps/meteor/client/views/admin/federationDashboard/FederationDashboardPage.tsx b/apps/meteor/client/views/admin/federationDashboard/FederationDashboardPage.tsx deleted file mode 100644 index 2486cde6719f5..0000000000000 --- a/apps/meteor/client/views/admin/federationDashboard/FederationDashboardPage.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import type { ReactElement } from 'react'; -import { useTranslation } from 'react-i18next'; - -import OverviewSection from './OverviewSection'; -import ServersSection from './ServersSection'; -import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; - -function FederationDashboardPage(): ReactElement { - const { t } = useTranslation(); - - return ( - - - - - - - - - - ); -} - -export default FederationDashboardPage; diff --git a/apps/meteor/client/views/admin/federationDashboard/FederationDashboardRoute.tsx b/apps/meteor/client/views/admin/federationDashboard/FederationDashboardRoute.tsx deleted file mode 100644 index cda2127b60008..0000000000000 --- a/apps/meteor/client/views/admin/federationDashboard/FederationDashboardRoute.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useRole } from '@rocket.chat/ui-contexts'; - -import FederationDashboardPage from './FederationDashboardPage'; -import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; - -const FederationDashboardRoute = () => { - const authorized = useRole('admin'); - - if (!authorized) { - return ; - } - - return ; -}; - -export default FederationDashboardRoute; diff --git a/apps/meteor/client/views/admin/federationDashboard/OverviewSection.stories.tsx b/apps/meteor/client/views/admin/federationDashboard/OverviewSection.stories.tsx deleted file mode 100644 index 0d3c5d38d472a..0000000000000 --- a/apps/meteor/client/views/admin/federationDashboard/OverviewSection.stories.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import type { Meta, StoryFn } from '@storybook/react'; - -import OverviewSection from './OverviewSection'; - -export default { - component: OverviewSection, - parameters: { - layout: 'centered', - controls: { hideNoControlsWarning: true }, - }, -} satisfies Meta; - -const Template: StoryFn = () => ; - -export const Example = Template.bind({}); -Example.parameters = { - serverContext: { - callMethod: { - 'federation:getOverviewData': async () => ({ - data: [ - { - title: 'Number_of_events', - value: 123, - }, - { - title: 'Number_of_federated_users', - value: 123, - }, - { - title: 'Number_of_federated_servers', - value: 123, - }, - ], - }), - }, - }, -}; - -export const Loading = Template.bind({}); -Loading.parameters = { - serverContext: { - callMethod: { - 'federation:getOverviewData': 'infinite', - }, - }, -}; - -export const Errored = Template.bind({}); -Errored.parameters = { - serverContext: { - callMethod: { - 'federation:getOverviewData': 'errored', - }, - }, -}; diff --git a/apps/meteor/client/views/admin/federationDashboard/OverviewSection.tsx b/apps/meteor/client/views/admin/federationDashboard/OverviewSection.tsx deleted file mode 100644 index 9275247c9ff45..0000000000000 --- a/apps/meteor/client/views/admin/federationDashboard/OverviewSection.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Box, Skeleton } from '@rocket.chat/fuselage'; -import { useMethod } from '@rocket.chat/ui-contexts'; -import { useQuery } from '@tanstack/react-query'; -import type { ReactElement, ReactNode } from 'react'; -import { useTranslation } from 'react-i18next'; - -import CounterSet from '../../../components/dataView/CounterSet'; - -const useOverviewData = (): [eventCount: ReactNode, userCount: ReactNode, serverCount: ReactNode] => { - const getFederationOverviewData = useMethod('federation:getOverviewData'); - - const result = useQuery({ - queryKey: ['admin/federation-dashboard/overview'], - queryFn: async () => getFederationOverviewData(), - refetchInterval: 10_000, - }); - - if (result.isPending) { - return [ - , - , - , - ]; - } - - if (result.isError) { - return [ - - Error - , - - Error - , - - Error - , - ]; - } - - const { data } = result.data; - - return [data[0].value, data[1].value, data[2].value]; -}; - -function OverviewSection(): ReactElement { - const { t } = useTranslation(); - - const [eventCount, userCount, serverCount] = useOverviewData(); - - return ( - - ); -} - -export default OverviewSection; diff --git a/apps/meteor/client/views/admin/federationDashboard/ServersSection.stories.tsx b/apps/meteor/client/views/admin/federationDashboard/ServersSection.stories.tsx deleted file mode 100644 index 1ddc311313824..0000000000000 --- a/apps/meteor/client/views/admin/federationDashboard/ServersSection.stories.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import type { Meta, StoryFn } from '@storybook/react'; - -import ServersSection from './ServersSection'; - -export default { - component: ServersSection, - parameters: { - layout: 'centered', - controls: { hideNoControlsWarning: true }, - }, -} satisfies Meta; - -const Template: StoryFn = () => ; - -export const Example = Template.bind({}); -Example.parameters = { - serverContext: { - callMethod: { - 'federation:getOverviewData': async () => ({ - data: [ - { - title: 'Number_of_events', - value: 123, - }, - { - title: 'Number_of_federated_users', - value: 123, - }, - { - title: 'Number_of_federated_servers', - value: 123, - }, - ], - }), - 'federation:getServers': async () => ({ - data: [ - { - _id: 'server-id-1', - domain: 'open.rocket.chat', - }, - { - _id: 'server-id-2', - domain: 'unstable.rocket.chat', - }, - ], - }), - }, - }, -}; - -export const Loading = Template.bind({}); -Loading.parameters = { - serverContext: { - callMethod: { - 'federation:getOverviewData': 'infinite', - 'federation:getServers': 'infinite', - }, - }, -}; - -export const Errored = Template.bind({}); -Errored.parameters = { - serverContext: { - callMethod: { - 'federation:getOverviewData': 'errored', - 'federation:getServers': 'errored', - }, - }, -}; diff --git a/apps/meteor/client/views/admin/federationDashboard/ServersSection.tsx b/apps/meteor/client/views/admin/federationDashboard/ServersSection.tsx deleted file mode 100644 index d00283e0d1ce9..0000000000000 --- a/apps/meteor/client/views/admin/federationDashboard/ServersSection.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Box, Throbber } from '@rocket.chat/fuselage'; -import { useMethod } from '@rocket.chat/ui-contexts'; -import { useQuery } from '@tanstack/react-query'; -import type { ReactElement } from 'react'; - -function ServersSection(): ReactElement | null { - const getFederationServers = useMethod('federation:getServers'); - - const result = useQuery({ - queryKey: ['admin/federation-dashboard/servers'], - queryFn: async () => getFederationServers(), - refetchInterval: 10_000, - }); - - if (result.isPending) { - return ; - } - - if (result.isError || result.data.data.length === 0) { - return null; - } - - const servers = result.data.data; - - return ( - -
    - {servers.map(({ domain }) => ( -
  • {domain}
  • - ))} -
-
- ); -} - -export default ServersSection; diff --git a/apps/meteor/client/views/admin/routes.tsx b/apps/meteor/client/views/admin/routes.tsx index d244d5e2f19be..d1a53d45f9078 100644 --- a/apps/meteor/client/views/admin/routes.tsx +++ b/apps/meteor/client/views/admin/routes.tsx @@ -202,11 +202,6 @@ registerAdminRoute('/reports', { component: lazy(() => import('./viewLogs/ViewLogsRoute')), }); -registerAdminRoute('/federation', { - name: 'federation-dashboard', - component: lazy(() => import('./federationDashboard/FederationDashboardRoute')), -}); - registerAdminRoute('/permissions/:context?/:_id?', { name: 'admin-permissions', component: lazy(() => import('./permissions/PermissionsRouter')), diff --git a/apps/meteor/client/views/directory/DirectoryPage.tsx b/apps/meteor/client/views/directory/DirectoryPage.tsx index d9a6b818a3497..687a7c3a2b825 100644 --- a/apps/meteor/client/views/directory/DirectoryPage.tsx +++ b/apps/meteor/client/views/directory/DirectoryPage.tsx @@ -15,7 +15,7 @@ const DirectoryPage = (): ReactElement => { const { t } = useTranslation(); const defaultTab = useSetting('Accounts_Directory_DefaultView', 'users'); - const federationEnabled = useSetting('FEDERATION_Enabled'); + const federationEnabled = false; // TODO old federation removed const tab = useRouteParameter('tab') as TabName | undefined; const router = useRouter(); diff --git a/apps/meteor/server/configuration/federation.ts b/apps/meteor/server/configuration/federation.ts deleted file mode 100644 index 616b266d04195..0000000000000 --- a/apps/meteor/server/configuration/federation.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { FederationKeys } from '@rocket.chat/models'; - -import { STATUS_ENABLED, STATUS_REGISTERING, STATUS_ERROR_REGISTERING, STATUS_DISABLED } from '../../app/federation/server/constants'; -import { updateStatus, updateEnabled, isRegisteringOrEnabled } from '../../app/federation/server/functions/helpers'; -import { enableCallbacks, disableCallbacks } from '../../app/federation/server/lib/callbacks'; -import { registerWithHub } from '../../app/federation/server/lib/dns'; -import { getFederationDiscoveryMethod } from '../../app/federation/server/lib/getFederationDiscoveryMethod'; -import { getFederationDomain } from '../../app/federation/server/lib/getFederationDomain'; -import { setupLogger } from '../../app/federation/server/lib/logger'; -import type { ICachedSettings } from '../../app/settings/server/CachedSettings'; - -export function configureFederation(settings: ICachedSettings): void { - const updateSettings = async function (): Promise { - // Get the key pair - - if (getFederationDiscoveryMethod() === 'hub' && !(await isRegisteringOrEnabled())) { - // Register with hub - try { - await updateStatus(STATUS_REGISTERING); - - await registerWithHub(getFederationDomain(), settings.get('Site_Url'), await FederationKeys.getPublicKeyString()); - - await updateStatus(STATUS_ENABLED); - } catch (err) { - // Disable federation - await updateEnabled(false); - - await updateStatus(STATUS_ERROR_REGISTERING); - } - return; - } - await updateStatus(STATUS_ENABLED); - }; - - // Add settings listeners - settings.watch('FEDERATION_Enabled', async function enableOrDisable(value) { - setupLogger.info(`Federation is ${value ? 'enabled' : 'disabled'}`); - - if (value) { - await updateSettings(); - - enableCallbacks(); - } else { - await updateStatus(STATUS_DISABLED); - - disableCallbacks(); - } - }); - - settings.watchMultiple(['FEDERATION_Discovery_Method', 'FEDERATION_Domain'], updateSettings); -} diff --git a/apps/meteor/server/configuration/index.ts b/apps/meteor/server/configuration/index.ts index 4e7ce6de4e925..491c410e26f4a 100644 --- a/apps/meteor/server/configuration/index.ts +++ b/apps/meteor/server/configuration/index.ts @@ -8,7 +8,6 @@ import { configureDirectReply } from './configureDirectReply'; import { configureIRC } from './configureIRC'; import { configureLogLevel } from './configureLogLevel'; import { configureSMTP } from './configureSMTP'; -import { configureFederation } from './federation'; import { configureLDAP } from './ldap'; import { configureOAuth } from './oauth'; import { configurePushNotifications } from './pushNotification'; @@ -28,7 +27,6 @@ export async function configureServer(settings: ICachedSettings) { configureBoilerplate(settings), configureDirectReply(settings), configureSMTP(settings), - configureFederation(settings), configureIRC(settings), ]); } diff --git a/apps/meteor/server/cron/federation.ts b/apps/meteor/server/cron/federation.ts deleted file mode 100644 index e78b9693d175e..0000000000000 --- a/apps/meteor/server/cron/federation.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { SettingValue } from '@rocket.chat/core-typings'; -import { eventTypes } from '@rocket.chat/core-typings'; -import { cronJobs } from '@rocket.chat/cron'; -import { Users, Settings } from '@rocket.chat/models'; - -import { resolveSRV, resolveTXT } from '../../app/federation/server/functions/resolveDNS'; -import { dispatchEvent } from '../../app/federation/server/handler'; -import { getFederationDomain } from '../../app/federation/server/lib/getFederationDomain'; -import { notifyOnSettingChangedById } from '../../app/lib/server/lib/notifyListener'; -import { settings, settingsRegistry } from '../../app/settings/server'; - -async function updateSetting(id: string, value: SettingValue | null): Promise { - if (value !== null) { - const setting = settings.get(id); - - if (setting === undefined) { - await settingsRegistry.add(id, value); - } else { - // TODO: audit - (await Settings.updateValueById(id, value)).modifiedCount && void notifyOnSettingChangedById(id); - } - } else { - await Settings.updateValueById(id, null); - } -} - -async function runFederation(): Promise { - // Get the settings - const siteUrl = settings.get('Site_Url') as string; - const { protocol } = new URL(siteUrl); - const rocketChatProtocol = protocol.slice(0, -1); - - const federationDomain = settings.get('FEDERATION_Domain') as string; - - // Load public key info - try { - const resolvedTXT = await resolveTXT(`rocketchat-public-key.${federationDomain}`); - await updateSetting('FEDERATION_ResolvedPublicKeyTXT', resolvedTXT); - } catch (err) { - await updateSetting('FEDERATION_ResolvedPublicKeyTXT', null); - } - - // Load legacy tcp protocol info - try { - const resolvedTXT = await resolveTXT(`rocketchat-tcp-protocol.${federationDomain}`); - await updateSetting('FEDERATION_ResolvedProtocolTXT', resolvedTXT); - } catch (err) { - await updateSetting('FEDERATION_ResolvedProtocolTXT', null); - } - - // Load SRV info - try { - // If there is a protocol entry on DNS, we use it - const protocol = (settings.get('FEDERATION_ResolvedProtocolTXT') as string) ? 'tcp' : rocketChatProtocol; - - const resolvedSRV = await resolveSRV(`_rocketchat._${protocol}.${federationDomain}`); - await updateSetting('FEDERATION_ResolvedSRV', JSON.stringify(resolvedSRV)); - } catch (err) { - await updateSetting('FEDERATION_ResolvedSRV', '{}'); - } - - // Test if federation is healthy - try { - void dispatchEvent([getFederationDomain()], { - type: eventTypes.PING, - }); - - await updateSetting('FEDERATION_Healthy', true); - } catch (err) { - await updateSetting('FEDERATION_Healthy', false); - } - - // If federation is healthy, check if there are remote users - if (settings.get('FEDERATION_Healthy') as boolean) { - const user = await Users.findOne({ isRemote: true }); - - await updateSetting('FEDERATION_Populated', !!user); - } -} - -export function federationCron(): void { - const name = 'Federation'; - - settings.watch('FEDERATION_Enabled', async (value) => { - if (!value) { - return cronJobs.remove(name); - } - - await cronJobs.add(name, '* * * * *', async () => runFederation()); - }); -} diff --git a/apps/meteor/server/importPackages.ts b/apps/meteor/server/importPackages.ts index d22fdf275fecc..e36807bf7959a 100644 --- a/apps/meteor/server/importPackages.ts +++ b/apps/meteor/server/importPackages.ts @@ -17,7 +17,6 @@ import '../app/emoji/server'; import '../app/emoji-custom/server'; import '../app/emoji-emojione/server'; import '../app/error-handler/server'; -import '../app/federation/server'; import '../app/file/server'; import '../app/file-upload/server'; import '../app/github-enterprise/server'; diff --git a/apps/meteor/server/methods/browseChannels.ts b/apps/meteor/server/methods/browseChannels.ts index 95256da858928..2b22d2cd74ed1 100644 --- a/apps/meteor/server/methods/browseChannels.ts +++ b/apps/meteor/server/methods/browseChannels.ts @@ -9,9 +9,6 @@ import { Meteor } from 'meteor/meteor'; import type { FindOptions, SortDirection } from 'mongodb'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; -import { federationSearchUsers } from '../../app/federation/server/handler'; -import { getFederationDomain } from '../../app/federation/server/lib/getFederationDomain'; -import { isFederationEnabled } from '../../app/federation/server/lib/isFederationEnabled'; import { settings } from '../../app/settings/server'; import { isTruthy } from '../../lib/isTruthy'; import { trim } from '../../lib/utils/stringUtils'; @@ -233,13 +230,7 @@ const findUsers = async ({ } if (workspace === 'external') { - const { cursor, totalCount } = Users.findPaginatedByActiveExternalUsersExcept( - text, - [], - options, - searchFields, - getFederationDomain(), - ); + const { cursor, totalCount } = Users.findPaginatedByActiveExternalUsersExcept(text, [], options, searchFields); const [results, total] = await Promise.all([cursor.toArray(), totalCount]); return { total, @@ -247,13 +238,7 @@ const findUsers = async ({ }; } - const { cursor, totalCount } = Users.findPaginatedByActiveLocalUsersExcept( - text, - [], - options, - searchFields, - getFederationDomain(), - ); + const { cursor, totalCount } = Users.findPaginatedByActiveLocalUsersExcept(text, [], options, searchFields); const [results, total] = await Promise.all([cursor.toArray(), totalCount]); return { total, @@ -279,29 +264,6 @@ const getUsers = async ( const { total, results } = await findUsers({ text, sort, pagination, workspace, viewFullOtherUserInfo }); - // Try to find federated users, when applicable - if (isFederationEnabled() && workspace === 'external' && text.indexOf('@') !== -1) { - const users = await federationSearchUsers(text); - - for (const user of users) { - if (results.find((e) => e._id === user._id)) { - continue; - } - - // Add the federated user to the results - results.unshift({ - _id: user._id, - username: user.username, - name: user.name, - bio: user.bio, - nickname: user.nickname, - emails: user.emails, - federation: user.federation, - isRemote: true, - }); - } - } - return { total, results, diff --git a/apps/meteor/server/methods/createDirectMessage.ts b/apps/meteor/server/methods/createDirectMessage.ts index 51847454fe614..bbd6b486a54eb 100644 --- a/apps/meteor/server/methods/createDirectMessage.ts +++ b/apps/meteor/server/methods/createDirectMessage.ts @@ -6,7 +6,6 @@ import { check, Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; -import { addUser } from '../../app/federation/server/functions/addUser'; import { createRoom } from '../../app/lib/server/functions/createRoom'; import { RateLimiterClass as RateLimiter } from '../../app/lib/server/lib/RateLimiter'; import { settings } from '../../app/settings/server'; @@ -40,32 +39,7 @@ export async function createDirectMessage( }); } - const users = await Promise.all( - usernames - .filter((username) => username !== me.username) - .map(async (username) => { - let to: IUser | null = await Users.findOneByUsernameIgnoringCase(username); - - // If the username does have an `@`, but does not exist locally, we create it first - if (!to && username.includes('@')) { - try { - to = await addUser(username); - } catch { - // no-op - } - if (!to) { - return username; - } - } - - if (!to) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'createDirectMessage', - }); - } - return to; - }), - ); + const users = await Promise.all(usernames.filter((username) => username !== me.username)); const roomUsers = excludeSelf ? users : [me, ...users]; // allow self-DMs diff --git a/apps/meteor/server/startup/cron.ts b/apps/meteor/server/startup/cron.ts index f1645f203286b..bed6b624f69b2 100644 --- a/apps/meteor/server/startup/cron.ts +++ b/apps/meteor/server/startup/cron.ts @@ -1,6 +1,5 @@ import { Logger } from '@rocket.chat/logger'; -import { federationCron } from '../cron/federation'; import { npsCron } from '../cron/nps'; import { oembedCron } from '../cron/oembed'; import { startCron } from '../cron/start'; @@ -13,6 +12,5 @@ const logger = new Logger('SyncedCron'); export const startCronJobs = async (): Promise => { await Promise.all([startCron(), oembedCron(), usageReportCron(logger), npsCron(), temporaryUploadCleanupCron(), videoConferencesCron()]); - federationCron(); userDataDownloadsCron(); }; diff --git a/apps/meteor/app/federation/server/startup/generateKeys.js b/apps/meteor/server/startup/generateKeys.ts similarity index 77% rename from apps/meteor/app/federation/server/startup/generateKeys.js rename to apps/meteor/server/startup/generateKeys.ts index da372335a6622..dbe8a5ba1e0ce 100644 --- a/apps/meteor/app/federation/server/startup/generateKeys.js +++ b/apps/meteor/server/startup/generateKeys.ts @@ -1,8 +1,8 @@ import { FederationKeys } from '@rocket.chat/models'; // Create key pair if needed -(async () => { +export async function generateFederationKeys() { if (!(await FederationKeys.getPublicKey())) { await FederationKeys.generateKeys(); } -})(); +} diff --git a/apps/meteor/server/startup/index.ts b/apps/meteor/server/startup/index.ts index 048735bba7527..cfc22d97afaae 100644 --- a/apps/meteor/server/startup/index.ts +++ b/apps/meteor/server/startup/index.ts @@ -4,6 +4,7 @@ import { startCronJobs } from './cron'; import './initialData'; import './serverRunning'; import './coreApps'; +import { generateFederationKeys } from './generateKeys'; import './presenceTroubleshoot'; import '../hooks'; import '../lib/rooms/roomTypes'; @@ -14,6 +15,8 @@ import { isRunningMs } from '../lib/isRunningMs'; export const startup = async () => { await performMigrationProcedure(); + await generateFederationKeys(); + setImmediate(() => startCronJobs()); // only starts network broker if running in micro services mode if (!isRunningMs()) { diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index 4f11ad9baf02e..6097656e9e65a 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -16,7 +16,6 @@ import type { CommandsEndpoints } from './v1/commands'; import type { CustomUserStatusEndpoints } from './v1/customUserStatus'; import type { DirectoryEndpoint } from './v1/directory'; import type { ImEndpoints, DmEndpoints } from './v1/dm'; -import type { DnsEndpoints } from './v1/dns'; import type { E2eEndpoints } from './v1/e2e'; import type { EmailInboxEndpoints } from './v1/email-inbox'; import type { EmojiCustomEndpoints } from './v1/emojiCustom'; @@ -60,7 +59,6 @@ export interface Endpoints CommandsEndpoints, CustomUserStatusEndpoints, DmEndpoints, - DnsEndpoints, DirectoryEndpoint, EmojiCustomEndpoints, GroupsEndpoints, diff --git a/packages/rest-typings/src/v1/dns.ts b/packages/rest-typings/src/v1/dns.ts deleted file mode 100644 index b3cdb4e1ff68c..0000000000000 --- a/packages/rest-typings/src/v1/dns.ts +++ /dev/null @@ -1,53 +0,0 @@ -import Ajv from 'ajv'; - -const ajv = new Ajv({ - coerceTypes: true, -}); - -type DnsResolveTxtProps = { - url: string; -}; - -const dnsResolveTxtPropsSchema = { - type: 'object', - properties: { - url: { - type: 'string', - }, - }, - required: ['url'], - additionalProperties: false, -}; - -export const isDnsResolveTxtProps = ajv.compile(dnsResolveTxtPropsSchema); - -type DnsResolveSrvProps = { - url: string; -}; - -const DnsResolveSrvSchema = { - type: 'object', - properties: { - url: { - type: 'string', - }, - }, - required: ['url'], - additionalProperties: false, -}; - -export const isDnsResolveSrvProps = ajv.compile(DnsResolveSrvSchema); - -export type DnsEndpoints = { - '/v1/dns.resolve.srv': { - GET: (params: DnsResolveSrvProps) => { - resolved: Record; - }; - }; - '/v1/dns.resolve.txt': { - POST: (params: DnsResolveTxtProps) => { - resolved: string; - // resolved: Record; - }; - }; -};