diff --git a/app/federation/client/admin/dashboard.html b/app/federation/client/admin/dashboard.html index 476cc9472df..ff303fd90ab 100644 --- a/app/federation/client/admin/dashboard.html +++ b/app/federation/client/admin/dashboard.html @@ -16,11 +16,10 @@ {{/each}}
- {{#each federationPeerStatuses}} + {{#each federationPeers}}
-
-
- {{peer}} +
+ {{domain}}
{{/each}} diff --git a/app/federation/client/admin/dashboard.js b/app/federation/client/admin/dashboard.js index 1979e798b26..e53325264d2 100644 --- a/app/federation/client/admin/dashboard.js +++ b/app/federation/client/admin/dashboard.js @@ -18,10 +18,7 @@ let templateInstance; // current template instance/context const updateOverviewData = () => { Meteor.call('federation:getOverviewData', (error, result) => { if (error) { - console.log(error); - return; - // return handleError(error); } const { data } = result; @@ -30,32 +27,29 @@ const updateOverviewData = () => { }); }; -const updatePeerStatuses = () => { - Meteor.call('federation:getPeerStatuses', (error, result) => { +const updateServers = () => { + Meteor.call('federation:getServers', (error, result) => { if (error) { - console.log(error); - return; - // return handleError(error); } const { data } = result; - templateInstance.federationPeerStatuses.set(data); + templateInstance.federationPeers.set(data); }); }; const updateData = () => { updateOverviewData(); - updatePeerStatuses(); + updateServers(); }; Template.dashboard.helpers({ federationOverviewData() { return templateInstance.federationOverviewData.get(); }, - federationPeerStatuses() { - return templateInstance.federationPeerStatuses.get(); + federationPeers() { + return templateInstance.federationPeers.get(); }, }); @@ -64,7 +58,7 @@ Template.dashboard.onCreated(function() { templateInstance = Template.instance(); this.federationOverviewData = new ReactiveVar(); - this.federationPeerStatuses = new ReactiveVar(); + this.federationPeers = new ReactiveVar(); }); Template.dashboard.onRendered(() => { diff --git a/app/federation/client/index.js b/app/federation/client/index.js index 14655cf4b6a..1a16fba9d1f 100644 --- a/app/federation/client/index.js +++ b/app/federation/client/index.js @@ -1,2 +1 @@ -import './messageTypes'; import './admin/dashboard'; diff --git a/app/federation/client/messageTypes.js b/app/federation/client/messageTypes.js deleted file mode 100644 index 573845cba35..00000000000 --- a/app/federation/client/messageTypes.js +++ /dev/null @@ -1,23 +0,0 @@ -import { MessageTypes } from '../../ui-utils/client'; - -// Register message types -MessageTypes.registerType({ - id: 'rejected-message-by-peer', - system: true, - message: 'This_message_was_rejected_by__peer__peer', - data(message) { - return { - peer: message.peer, - }; - }, -}); -MessageTypes.registerType({ - id: 'peer-does-not-exist', - system: true, - message: 'The_peer__peer__does_not_exist', - data(message) { - return { - peer: message.peer, - }; - }, -}); diff --git a/app/federation/server/PeerClient.js b/app/federation/server/PeerClient.js deleted file mode 100644 index 38ceabf807a..00000000000 --- a/app/federation/server/PeerClient.js +++ /dev/null @@ -1,638 +0,0 @@ -import qs from 'querystring'; - -import { Meteor } from 'meteor/meteor'; - -import { updateStatus } from './settingsUpdater'; -import { logger } from './logger'; -import { FederatedMessage, FederatedRoom, FederatedUser } from './federatedResources'; -import { callbacks } from '../../callbacks/server'; -import { settings } from '../../settings/server'; -import { FederationEvents, FederationKeys, Messages, Rooms, Subscriptions, Users } from '../../models/server'; - -import { Federation } from '.'; - -export class PeerClient { - constructor() { - this.config = {}; - - this.enabled = false; - - // Keep resources we should skip callbacks - this.callbacksToSkip = {}; - } - - setConfig(config) { - // General - this.config = config; - - // Setup HubPeer - const { hub: { url } } = this.config; - - // Remove trailing slash - this.HubPeer = { url }; - - // Set the local peer - this.peer = { - domain: this.config.peer.domain, - url: this.config.peer.url, - public_key: this.config.peer.public_key, - cloud_token: this.config.cloud.token, - }; - } - - log(message) { - logger.peerClient.info(message); - } - - disable() { - this.log('Disabling...'); - - this.enabled = false; - } - - enable() { - this.log('Enabling...'); - - this.enabled = true; - } - - start() { - this.setupCallbacks(); - } - - // ########### - // - // Registering - // - // ########### - register() { - if (this.config.hub.active) { - updateStatus('Registering with Hub...'); - - return Federation.peerDNS.register(this.peer); - } - - return true; - } - - // ################### - // - // Callback management - // - // ################### - addCallbackToSkip(callback, resourceId) { - this.callbacksToSkip[`${ callback }_${ resourceId }`] = true; - } - - skipCallbackIfNeeded(callback, resource) { - const { federation } = resource; - - if (!federation) { return false; } - - const { _id } = federation; - - const callbackName = `${ callback }_${ _id }`; - - const skipCallback = this.callbacksToSkip[callbackName]; - - delete this.callbacksToSkip[callbackName]; - - this.log(`${ callbackName } callback ${ skipCallback ? '' : 'not ' }skipped`); - - return skipCallback; - } - - wrapEnabled(callbackHandler) { - return function(...parameters) { - if (!this.enabled) { return; } - - callbackHandler.apply(this, parameters); - }.bind(this); - } - - setupCallbacks() { - // Accounts.onLogin(onLoginCallbackHandler.bind(this)); - // Accounts.onLogout(onLogoutCallbackHandler.bind(this)); - - FederationEvents.on('createEvent', this.wrapEnabled(this.onCreateEvent.bind(this))); - - callbacks.add('afterCreateDirectRoom', this.wrapEnabled(this.afterCreateDirectRoom.bind(this)), callbacks.priority.LOW, 'federation-create-direct-room'); - callbacks.add('afterCreateRoom', this.wrapEnabled(this.afterCreateRoom.bind(this)), callbacks.priority.LOW, 'federation-create-room'); - callbacks.add('afterSaveRoomSettings', this.wrapEnabled(this.afterSaveRoomSettings.bind(this)), callbacks.priority.LOW, 'federation-after-save-room-settings'); - callbacks.add('afterAddedToRoom', this.wrapEnabled(this.afterAddedToRoom.bind(this)), callbacks.priority.LOW, 'federation-added-to-room'); - callbacks.add('beforeLeaveRoom', this.wrapEnabled(this.beforeLeaveRoom.bind(this)), callbacks.priority.LOW, 'federation-leave-room'); - callbacks.add('beforeRemoveFromRoom', this.wrapEnabled(this.beforeRemoveFromRoom.bind(this)), callbacks.priority.LOW, 'federation-remove-from-room'); - callbacks.add('afterSaveMessage', this.wrapEnabled(this.afterSaveMessage.bind(this)), callbacks.priority.LOW, 'federation-save-message'); - callbacks.add('afterDeleteMessage', this.wrapEnabled(this.afterDeleteMessage.bind(this)), callbacks.priority.LOW, 'federation-delete-message'); - callbacks.add('afterReadMessages', this.wrapEnabled(this.afterReadMessages.bind(this)), callbacks.priority.LOW, 'federation-read-messages'); - callbacks.add('afterSetReaction', this.wrapEnabled(this.afterSetReaction.bind(this)), callbacks.priority.LOW, 'federation-after-set-reaction'); - callbacks.add('afterUnsetReaction', this.wrapEnabled(this.afterUnsetReaction.bind(this)), callbacks.priority.LOW, 'federation-after-unset-reaction'); - callbacks.add('afterMuteUser', this.wrapEnabled(this.afterMuteUser.bind(this)), callbacks.priority.LOW, 'federation-mute-user'); - callbacks.add('afterUnmuteUser', this.wrapEnabled(this.afterUnmuteUser.bind(this)), callbacks.priority.LOW, 'federation-unmute-user'); - - this.log('Callbacks set'); - } - - // ################ - // - // Event management - // - // ################ - propagateEvent(e) { - this.log(`propagateEvent: ${ e.t }`); - - const { peer: domain, options: eventOptions } = e; - - const peer = Federation.peerDNS.searchPeer(domain); - - if (!peer || !peer.public_key) { - this.log(`Could not find valid peer:${ domain }`); - - FederationEvents.setEventAsErrored(e, 'Could not find valid peer'); - } else { - try { - const stringPayload = JSON.stringify({ event: e }); - - // Encrypt with the peer's public key - let payload = FederationKeys.loadKey(peer.public_key, 'public').encrypt(stringPayload); - - // Encrypt with the local private key - payload = Federation.privateKey.encryptPrivate(payload); - - Federation.peerHTTP.request(peer, 'POST', '/api/v1/federation.events', { payload }, eventOptions.retry || { total: 5, stepSize: 500, stepMultiplier: 10 }); - - FederationEvents.setEventAsFullfilled(e); - } catch (err) { - this.log(`[${ e.t }] Event could not be sent to peer:${ domain }`); - - if (err.response) { - const { response: { data: error } } = err; - - if (error.errorType === 'error-app-prevented-sending') { - const { payload: { - message: { - rid: roomId, - u: { - username, - federation: { _id: userId }, - }, - }, - } } = e; - - const localUsername = username.split('@')[0]; - - // Create system message - Messages.createRejectedMessageByPeer(roomId, localUsername, { - u: { - _id: userId, - username: localUsername, - }, - peer: domain, - }); - - return FederationEvents.setEventAsErrored(e, err.error, true); - } - } - - if (err.error === 'federation-peer-does-not-exist') { - const { payload: { - message: { - rid: roomId, - u: { - username, - federation: { _id: userId }, - }, - }, - } } = e; - - const localUsername = username.split('@')[0]; - - // Create system message - Messages.createPeerDoesNotExist(roomId, localUsername, { - u: { - _id: userId, - username: localUsername, - }, - peer: domain, - }); - - return FederationEvents.setEventAsErrored(e, err.error, true); - } - - return FederationEvents.setEventAsErrored(e, `Could not send request to ${ domain }`); - } - } - } - - onCreateEvent(e) { - this.propagateEvent(e); - } - - resendUnfulfilledEvents() { - // Should we use queues in here? - const events = FederationEvents.getUnfulfilled(); - - events.forEach((e) => this.propagateEvent(e)); - } - - // ##### - // - // Users - // - // ##### - findUsers(identifier, options = {}) { - const [username, domain] = identifier.split('@'); - - const { peer: { domain: localPeerDomain } } = this; - - let peer = null; - - try { - peer = Federation.peerDNS.searchPeer(options.domainOverride || domain); - } catch (err) { - this.log(`Could not find peer using domain:${ domain }`); - throw new Meteor.Error('federation-peer-does-not-exist', `Could not find peer using domain:${ domain }`); - } - - try { - const { data: { federatedUsers: remoteFederatedUsers } } = Federation.peerHTTP.request(peer, 'GET', `/api/v1/federation.users?${ qs.stringify({ username, domain, usernameOnly: options.usernameOnly }) }`); - - const federatedUsers = []; - - for (const federatedUser of remoteFederatedUsers) { - federatedUsers.push(new FederatedUser(localPeerDomain, federatedUser.user)); - } - - return federatedUsers; - } catch (err) { - this.log(`Could not find user:${ username } at ${ peer.domain }`); - throw new Meteor.Error('federation-user-does-not-exist', `Could not find user:${ identifier } at ${ peer.domain }`); - } - } - - // ####### - // - // Uploads - // - // ####### - getUpload(options) { - const { identifier: domain, localMessage: { file: { _id: fileId } } } = options; - - let peer = null; - - try { - peer = Federation.peerDNS.searchPeer(domain); - } catch (err) { - this.log(`Could not find peer using domain:${ domain }`); - throw new Meteor.Error('federation-peer-does-not-exist', `Could not find peer using domain:${ domain }`); - } - - const { data: { upload, buffer } } = Federation.peerHTTP.request(peer, 'GET', `/api/v1/federation.uploads?${ qs.stringify({ upload_id: fileId }) }`); - - return { upload, buffer: Buffer.from(buffer) }; - } - - // ################# - // - // Callback handlers - // - // ################# - afterCreateDirectRoom(room, { from: owner }) { - this.log('afterCreateDirectRoom'); - - const { peer: { domain: localPeerDomain } } = this; - - // Check if room is federated - if (!FederatedRoom.isFederated(localPeerDomain, room, { checkUsingUsers: true })) { return room; } - - const federatedRoom = new FederatedRoom(localPeerDomain, room, { owner }); - - // Check if this should be skipped - if (this.skipCallbackIfNeeded('afterCreateDirectRoom', federatedRoom.getLocalRoom())) { return room; } - - // Load federated users - federatedRoom.loadUsers(); - - // Refresh room's federation - federatedRoom.refreshFederation(); - - FederationEvents.directRoomCreated(federatedRoom, { skipPeers: [localPeerDomain] }); - - return room; - } - - afterCreateRoom(roomOwner, room) { - this.log('afterCreateRoom'); - - const { _id: ownerId } = roomOwner; - - const { peer: { domain: localPeerDomain } } = this; - - // Check if room is federated - if (!FederatedRoom.isFederated(localPeerDomain, room, { checkUsingUsers: true })) { return roomOwner; } - - const owner = Users.findOneById(ownerId); - - const federatedRoom = new FederatedRoom(localPeerDomain, room, { owner }); - - // Check if this should be skipped - if (this.skipCallbackIfNeeded('afterCreateRoom', federatedRoom.getLocalRoom())) { return roomOwner; } - - // Load federated users - federatedRoom.loadUsers(); - - // Refresh room's federation - federatedRoom.refreshFederation(); - - FederationEvents.roomCreated(federatedRoom, { skipPeers: [localPeerDomain] }); - - return roomOwner; - } - - afterSaveRoomSettings(/* room */) { - this.log('afterSaveRoomSettings - NOT IMPLEMENTED'); - } - - afterAddedToRoom(users, room) { - this.log('afterAddedToRoom'); - - const { user: userWhoJoined, inviter: userWhoInvited } = users; - - // Check if this should be skipped - if (this.skipCallbackIfNeeded('afterAddedToRoom', userWhoJoined)) { return users; } - - const { peer: { domain: localPeerDomain } } = this; - - // Check if room or user who joined are federated - if ((!userWhoJoined.federation || userWhoJoined.federation.peer === localPeerDomain) - && !FederatedRoom.isFederated(localPeerDomain, room)) { - return users; - } - - const extras = {}; - - // If the room is not federated and has an owner - if (!room.federation) { - let ownerId; - - // If the room does not have an owner, get the first user subscribed to that room - if (!room.u) { - const userSubscription = Subscriptions.findOne({ rid: room._id }, { - sort: { - ts: 1, - }, - }); - - ownerId = userSubscription.u._id; - } else { - ownerId = room.u._id; - } - - extras.owner = Users.findOneById(ownerId); - } - - const federatedRoom = new FederatedRoom(localPeerDomain, room, extras); - - // Load federated users - federatedRoom.loadUsers(); - - // Refresh room's federation - federatedRoom.refreshFederation(); - - // If the user who joined is from a different peer... - if (userWhoJoined.federation && userWhoJoined.federation.peer !== localPeerDomain) { - // ...create a "create room" event for that peer - FederationEvents.roomCreated(federatedRoom, { peers: [userWhoJoined.federation.peer] }); - } - - // Then, create a "user join/added" event to the other peers - const federatedUserWhoJoined = FederatedUser.loadOrCreate(localPeerDomain, userWhoJoined); - - if (userWhoInvited) { - const federatedInviter = FederatedUser.loadOrCreate(localPeerDomain, userWhoInvited); - - FederationEvents.userAdded(federatedRoom, federatedUserWhoJoined, federatedInviter, { skipPeers: [localPeerDomain] }); - } else { - FederationEvents.userJoined(federatedRoom, federatedUserWhoJoined, { skipPeers: [localPeerDomain] }); - } - - return users; - } - - beforeLeaveRoom(userWhoLeft, room) { - this.log('beforeLeaveRoom'); - - // Check if this should be skipped - if (this.skipCallbackIfNeeded('beforeLeaveRoom', userWhoLeft)) { return userWhoLeft; } - - const { peer: { domain: localPeerDomain } } = this; - - // Check if room is federated - if (!FederatedRoom.isFederated(localPeerDomain, room)) { return userWhoLeft; } - - const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id); - - const federatedUserWhoLeft = FederatedUser.loadByFederationId(localPeerDomain, userWhoLeft.federation._id); - - // Then, create a "user left" event to the other peers - FederationEvents.userLeft(federatedRoom, federatedUserWhoLeft, { skipPeers: [localPeerDomain] }); - - // Load federated users - federatedRoom.loadUsers(); - - // Refresh room's federation - federatedRoom.refreshFederation(); - - return userWhoLeft; - } - - beforeRemoveFromRoom(users, room) { - this.log('beforeRemoveFromRoom'); - - const { removedUser, userWhoRemoved } = users; - - // Check if this should be skipped - if (this.skipCallbackIfNeeded('beforeRemoveFromRoom', removedUser)) { return users; } - - const { peer: { domain: localPeerDomain } } = this; - - // Check if room is federated - if (!FederatedRoom.isFederated(localPeerDomain, room)) { return users; } - - const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id); - - const federatedRemovedUser = FederatedUser.loadByFederationId(localPeerDomain, removedUser.federation._id); - - const federatedUserWhoRemoved = FederatedUser.loadByFederationId(localPeerDomain, userWhoRemoved.federation._id); - - FederationEvents.userRemoved(federatedRoom, federatedRemovedUser, federatedUserWhoRemoved, { skipPeers: [localPeerDomain] }); - - // Load federated users - federatedRoom.loadUsers(); - - // Refresh room's federation - federatedRoom.refreshFederation(); - - return users; - } - - afterSaveMessage(message, room) { - this.log('afterSaveMessage'); - - // Check if this should be skipped - if (this.skipCallbackIfNeeded('afterSaveMessage', message)) { return message; } - - const { peer: { domain: localPeerDomain } } = this; - - // Check if room is federated - if (!FederatedRoom.isFederated(localPeerDomain, room)) { return message; } - - const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id); - - const federatedMessage = FederatedMessage.loadOrCreate(localPeerDomain, message); - - // If editedAt exists, it means it is an update - if (message.editedAt) { - const user = Users.findOneById(message.editedBy._id); - - const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, user.federation._id); - - FederationEvents.messageUpdated(federatedRoom, federatedMessage, federatedUser, { skipPeers: [localPeerDomain] }); - } else { - FederationEvents.messageCreated(federatedRoom, federatedMessage, { skipPeers: [localPeerDomain] }); - } - - return message; - } - - afterDeleteMessage(message) { - this.log('afterDeleteMessage'); - - // Check if this should be skipped - if (this.skipCallbackIfNeeded('afterDeleteMessage', message)) { return message; } - - const { peer: { domain: localPeerDomain } } = this; - - const room = Rooms.findOneById(message.rid); - - // Check if room is federated - if (!FederatedRoom.isFederated(localPeerDomain, room)) { return message; } - - const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id); - - const federatedMessage = new FederatedMessage(localPeerDomain, message); - - FederationEvents.messageDeleted(federatedRoom, federatedMessage, { skipPeers: [localPeerDomain] }); - - return message; - } - - afterReadMessages(roomId, { userId }) { - this.log('afterReadMessages'); - - if (!settings.get('Message_Read_Receipt_Enabled')) { this.log('Skipping: read receipts are not enabled'); return roomId; } - - const { peer: { domain: localPeerDomain } } = this; - - const room = Rooms.findOneById(roomId); - - // Check if room is federated - if (!FederatedRoom.isFederated(localPeerDomain, room)) { return roomId; } - - const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id); - - if (this.skipCallbackIfNeeded('afterReadMessages', federatedRoom.getLocalRoom())) { return roomId; } - - const user = Users.findOneById(userId); - - const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, user.federation._id); - - FederationEvents.messagesRead(federatedRoom, federatedUser, { skipPeers: [localPeerDomain] }); - - return roomId; - } - - afterSetReaction(message, { user, reaction, shouldReact }) { - this.log('afterSetReaction'); - - const room = Rooms.findOneById(message.rid); - - const { peer: { domain: localPeerDomain } } = this; - - // Check if room is federated - if (!FederatedRoom.isFederated(localPeerDomain, room)) { return message; } - - const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, user.federation._id); - - const federatedMessage = FederatedMessage.loadByFederationId(localPeerDomain, message.federation._id); - - const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id); - - FederationEvents.messagesSetReaction(federatedRoom, federatedMessage, federatedUser, reaction, shouldReact, { skipPeers: [localPeerDomain] }); - - return message; - } - - afterUnsetReaction(message, { user, reaction, shouldReact }) { - this.log('afterUnsetReaction'); - - const room = Rooms.findOneById(message.rid); - - const { peer: { domain: localPeerDomain } } = this; - - // Check if room is federated - if (!FederatedRoom.isFederated(localPeerDomain, room)) { return message; } - - const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, user.federation._id); - - const federatedMessage = FederatedMessage.loadByFederationId(localPeerDomain, message.federation._id); - - const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id); - - FederationEvents.messagesUnsetReaction(federatedRoom, federatedMessage, federatedUser, reaction, shouldReact, { skipPeers: [localPeerDomain] }); - - return message; - } - - afterMuteUser(users, room) { - this.log('afterMuteUser'); - - const { mutedUser, fromUser } = users; - - const { peer: { domain: localPeerDomain } } = this; - - // Check if room is federated - if (!FederatedRoom.isFederated(localPeerDomain, room)) { return users; } - - const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id); - - const federatedMutedUser = FederatedUser.loadByFederationId(localPeerDomain, mutedUser.federation._id); - - const federatedUserWhoMuted = FederatedUser.loadByFederationId(localPeerDomain, fromUser.federation._id); - - FederationEvents.userMuted(federatedRoom, federatedMutedUser, federatedUserWhoMuted, { skipPeers: [localPeerDomain] }); - - return users; - } - - afterUnmuteUser(users, room) { - this.log('afterUnmuteUser'); - - const { unmutedUser, fromUser } = users; - - const { peer: { domain: localPeerDomain } } = this; - - // Check if room is federated - if (!FederatedRoom.isFederated(localPeerDomain, room)) { return users; } - - const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id); - - const federatedUnmutedUser = FederatedUser.loadByFederationId(localPeerDomain, unmutedUser.federation._id); - - const federatedUserWhoUnmuted = FederatedUser.loadByFederationId(localPeerDomain, fromUser.federation._id); - - FederationEvents.userUnmuted(federatedRoom, federatedUnmutedUser, federatedUserWhoUnmuted, { skipPeers: [localPeerDomain] }); - - return users; - } -} diff --git a/app/federation/server/PeerDNS.js b/app/federation/server/PeerDNS.js deleted file mode 100644 index ace2721769c..00000000000 --- a/app/federation/server/PeerDNS.js +++ /dev/null @@ -1,185 +0,0 @@ -import dns from 'dns'; - -import { Meteor } from 'meteor/meteor'; - - -import { logger } from './logger'; -import { updateStatus } from './settingsUpdater'; -import { FederationDNSCache } from '../../models'; - -import { Federation } from '.'; - -const dnsResolveSRV = Meteor.wrapAsync(dns.resolveSrv); -const dnsResolveTXT = Meteor.wrapAsync(dns.resolveTxt); - -export class PeerDNS { - constructor() { - this.config = {}; - } - - setConfig(config) { - // General - this.config = config; - - // Setup HubPeer - const { hub: { url } } = config; - this.HubPeer = { url }; - } - - log(message) { - logger.dns.info(message); - } - - // ######## - // - // Register - // - // ######## - register(peerConfig) { - const { uniqueId, domain, url, public_key, cloud_token } = peerConfig; - - this.log(`Registering peer with domain ${ domain }...`); - - let headers; - if (cloud_token && cloud_token !== '') { - headers = { Authorization: `Bearer ${ cloud_token }` }; - } - - // Attempt to register peer - try { - Federation.peerHTTP.request(this.HubPeer, 'POST', '/api/v1/peers', { uniqueId, domain, url, public_key }, { total: 5, stepSize: 1000, tryToUpdateDNS: false }, headers); - - this.log('Peer registered!'); - - updateStatus('Running, registered to Hub'); - - return true; - } catch (err) { - this.log(err); - - this.log('Could not register peer'); - - return false; - } - } - - // ############# - // - // Peer Handling - // - // ############# - searchPeer(domain) { - this.log(`searchPeer: ${ domain }`); - - let peer = FederationDNSCache.findOneByDomain(domain); - - // Try to lookup at the DNS Cache - if (!peer) { - try { - this.updatePeerDNS(domain); - - peer = FederationDNSCache.findOneByDomain(domain); - } catch (err) { - this.log(`Could not find peer for domain ${ domain }`); - } - } - - return peer; - } - - getPeerUsingDNS(domain) { - this.log(`getPeerUsingDNS: ${ domain }`); - - // Try searching by DNS first - const srvEntries = dnsResolveSRV(`_rocketchat._tcp.${ domain }`); - - const [srvEntry] = srvEntries; - - // Get the protocol from the TXT record, if exists - let protocol = 'https'; - - try { - const protocolTxtRecords = dnsResolveTXT(`rocketchat-protocol.${ domain }`); - - protocol = protocolTxtRecords[0][0].toLowerCase() === 'http' ? 'http' : 'https'; - } catch (err) { - // Ignore the error if the rocketchat-protocol TXT entry does not exist - } - - - // Get the public key from the TXT record - const publicKeyTxtRecords = dnsResolveTXT(`rocketchat-public-key.${ domain }`); - - // Get the first TXT record, this subdomain should have only a single record - const publicKey = publicKeyTxtRecords[0].join(''); - - return { - domain, - url: `${ protocol }://${ srvEntry.name }:${ srvEntry.port }`, - public_key: publicKey, - }; - } - - getPeerUsingHub(domain) { - this.log(`getPeerUsingHub: ${ domain }`); - - // If there is no DNS entry for that, get from the Hub - const { data: { peer } } = Federation.peerHTTP.simpleRequest(this.HubPeer, 'GET', `/api/v1/peers?search=${ domain }`); - - return peer; - } - - // ############## - // - // DNS Management - // - // ############## - updatePeerDNS(domain) { - this.log(`updatePeerDNS: ${ domain }`); - - let peer = null; - - try { - peer = this.getPeerUsingDNS(domain); - } catch (err) { - if (['ENODATA', 'ENOTFOUND'].indexOf(err.code) === -1) { - this.log(err); - - throw new Error(`Error trying to fetch SRV DNS entries for ${ domain }`); - } - - try { - peer = this.getPeerUsingHub(domain); - } catch (err) { - throw new Error(`Could not find a peer with domain ${ domain } using the hub`); - } - } - - this.updateDNSCache.call(this, peer); - - return peer; - } - - updateDNSEntry(peer) { - this.log('updateDNSEntry'); - - const { domain } = peer; - - delete peer._id; - - // Make sure public_key has no line breaks - peer.public_key = peer.public_key.replace(/\n|\r/g, ''); - - return FederationDNSCache.upsert({ domain }, peer); - } - - updateDNSCache(peers) { - this.log('updateDNSCache'); - - peers = Array.isArray(peers) ? peers : [peers]; - - for (const peer of peers) { - this.updateDNSEntry.call(this, peer); - } - } -} diff --git a/app/federation/server/PeerHTTP/PeerHTTP.js b/app/federation/server/PeerHTTP/PeerHTTP.js deleted file mode 100644 index fbcb1e72962..00000000000 --- a/app/federation/server/PeerHTTP/PeerHTTP.js +++ /dev/null @@ -1,100 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { HTTP } from 'meteor/http'; - -import { skipRetryOnSpecificError, delay } from './utils'; -import { logger } from '../logger'; - -import { Federation } from '..'; - - -export class PeerHTTP { - constructor() { - this.config = {}; - } - - setConfig(config) { - // General - this.config = config; - } - - log(message) { - logger.http.info(message); - } - - // - // Direct request - simpleRequest(peer, method, uri, body, headers) { - const { url: serverBaseURL } = peer; - - const url = `${ serverBaseURL }${ uri }`; - - let data = null; - - if (method === 'POST' || method === 'PUT') { - data = body; - } - - this.log(`Sending request: ${ method } - ${ url }`); - - return HTTP.call(method, url, { data, timeout: 2000, headers: { ...headers, 'x-federation-domain': this.config.peer.domain } }); - } - - // - // Request trying to find DNS entries - request(peer, method, uri, body, retryInfo = {}, headers = {}) { - // Normalize retry info - retryInfo = { - total: retryInfo.total || 1, - stepSize: retryInfo.stepSize || 100, - stepMultiplier: retryInfo.stepMultiplier || 1, - tryToUpdateDNS: retryInfo.tryToUpdateDNS === undefined ? true : retryInfo.tryToUpdateDNS, - DNSUpdated: false, - }; - - for (let i = 0; i <= retryInfo.total; i++) { - try { - return this.simpleRequest(peer, method, uri, body, headers); - } catch (err) { - try { - if (retryInfo.tryToUpdateDNS && !retryInfo.DNSUpdated) { - i--; - - retryInfo.DNSUpdated = true; - - this.log(`Trying to update local DNS cache for peer:${ peer.domain }`); - - peer = Federation.peerDNS.updatePeerDNS(peer.domain); - - continue; - } - } catch (err) { - if (err.response && err.response.statusCode === 404) { - throw new Meteor.Error('federation-peer-does-not-exist', 'Peer does not exist'); - } - } - - // Check if we need to skip due to specific error - const { skip: skipOnSpecificError, error: specificError } = skipRetryOnSpecificError(err); - if (skipOnSpecificError) { - this.log(`Retry: skipping due to specific error: ${ specificError }`); - - throw err; - } - - if (i === retryInfo.total - 1) { - // Throw the error, as we could not fulfill the request - this.log('Retry: could not fulfill the request'); - - throw err; - } - - const timeToRetry = retryInfo.stepSize * (i + 1) * retryInfo.stepMultiplier; - - this.log(`Trying again in ${ timeToRetry / 1000 }s: ${ method } - ${ uri }`); - - // Otherwise, wait and try again - delay(timeToRetry); - } - } - } -} diff --git a/app/federation/server/PeerHTTP/index.js b/app/federation/server/PeerHTTP/index.js deleted file mode 100644 index 3c9e957f1cc..00000000000 --- a/app/federation/server/PeerHTTP/index.js +++ /dev/null @@ -1 +0,0 @@ -export { PeerHTTP } from './PeerHTTP'; diff --git a/app/federation/server/PeerHTTP/utils.js b/app/federation/server/PeerHTTP/utils.js deleted file mode 100644 index 7cc6fc17709..00000000000 --- a/app/federation/server/PeerHTTP/utils.js +++ /dev/null @@ -1,16 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -// Should skip the retry if the error is one of the below? -const errorsToSkipRetrying = ['error-app-prevented-sending', 'error-decrypt']; - -export function skipRetryOnSpecificError(err) { - err = err && err.response && err.response.data && err.response.data.errorType; - return { skip: errorsToSkipRetrying.includes(err), error: err }; -} - -// Delay method to wait a little bit before retrying -export const delay = Meteor.wrapAsync(function(ms, callback) { - Meteor.setTimeout(function() { - callback(null); - }, ms); -}); diff --git a/app/federation/server/PeerPinger.js b/app/federation/server/PeerPinger.js deleted file mode 100644 index aa3bcbfa8a4..00000000000 --- a/app/federation/server/PeerPinger.js +++ /dev/null @@ -1,37 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import moment from 'moment'; - -import { logger } from './logger'; -import { ping } from './methods/ping'; -import { FederationPeers } from '../../models'; - - -export class PeerPinger { - constructor() { - this.config = { - pingInterval: 5000, - }; - - this.peers = []; - } - - log(message) { - logger.pinger.info(message); - } - - start() { - this.pingAllPeers(); - } - - pingAllPeers() { - const lastSeenAt = moment().subtract(10, 'm').toDate(); - - const peers = FederationPeers.find({ $or: [{ last_seen_at: null }, { last_seen_at: { $lte: lastSeenAt } }] }).fetch(); - - const pingResults = ping(peers.map((p) => p.peer)); - - FederationPeers.updateStatuses(pingResults); - - Meteor.setTimeout(this.pingAllPeers.bind(this), this.config.pingInterval); - } -} diff --git a/app/federation/server/PeerServer/PeerServer.js b/app/federation/server/PeerServer/PeerServer.js deleted file mode 100644 index f6b2ecd5b79..00000000000 --- a/app/federation/server/PeerServer/PeerServer.js +++ /dev/null @@ -1,404 +0,0 @@ -import { callbacks } from '../../../callbacks'; -import { setReaction } from '../../../reactions/server'; -import { addUserToRoom, removeUserFromRoom, deleteMessage } from '../../../lib'; -import { Rooms, Subscriptions, FederationPeers } from '../../../models'; -import { FederatedMessage, FederatedRoom, FederatedUser } from '../federatedResources'; -import { logger } from '../logger.js'; - -import { Federation } from '..'; - -export class PeerServer { - constructor() { - this.config = {}; - this.enabled = false; - } - - setConfig(config) { - // General - this.config = config; - } - - log(message) { - logger.peerServer.info(message); - } - - disable() { - this.log('Disabling...'); - - this.enabled = false; - } - - enable() { - this.log('Enabling...'); - - this.enabled = true; - } - - start() { - this.log('Routes are set'); - } - - handleDirectRoomCreatedEvent(e) { - this.log('handleDirectRoomCreatedEvent'); - - const { peer: { domain: localPeerDomain } } = this.config; - - const { payload: { room, owner, users } } = e; - - // Load the federated room - const federatedRoom = new FederatedRoom(localPeerDomain, room, { owner }); - - // Set users - federatedRoom.setUsers(users); - - // Create, if needed, all room's users - federatedRoom.createUsers(); - - // Then, create the room, if needed - federatedRoom.create(); - - // Refresh federation peers - FederationPeers.refreshPeers(localPeerDomain); - } - - handleRoomCreatedEvent(e) { - this.log('handleRoomCreatedEvent'); - - const { peer: { domain: localPeerDomain } } = this.config; - - const { payload: { room, owner, users } } = e; - - // Load the federated room - const federatedRoom = new FederatedRoom(localPeerDomain, room, { owner }); - - // Set users - federatedRoom.setUsers(users); - - // Create, if needed, all room's users - federatedRoom.createUsers(); - - // Then, create the room, if needed - federatedRoom.create(true); - - // Refresh federation peers - FederationPeers.refreshPeers(localPeerDomain); - } - - handleUserJoinedEvent(e) { - this.log('handleUserJoinedEvent'); - - const { peer: { domain: localPeerDomain } } = this.config; - - const { payload: { federated_room_id, user } } = e; - - // Load the federated room - const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id); - - // Create the user, if needed - const federatedUser = FederatedUser.loadOrCreate(localPeerDomain, user); - const localUser = federatedUser.create(); - - // Callback management - Federation.peerClient.addCallbackToSkip('afterAddedToRoom', federatedUser.getFederationId()); - - // Add the user to the room - addUserToRoom(federatedRoom.room._id, localUser, null, false); - - // Load federated users - federatedRoom.loadUsers(); - - // Refresh room's federation - federatedRoom.refreshFederation(); - - // Refresh federation peers - FederationPeers.refreshPeers(localPeerDomain); - } - - handleUserAddedEvent(e) { - this.log('handleUserAddedEvent'); - - const { peer: { domain: localPeerDomain } } = this.config; - - const { payload: { federated_room_id, federated_inviter_id, user } } = e; - - // Load the federated room - const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id); - - // Load the inviter - const federatedInviter = FederatedUser.loadByFederationId(localPeerDomain, federated_inviter_id); - - if (!federatedInviter) { - throw new Error('Inviting user does not exist'); - } - - const localInviter = federatedInviter.getLocalUser(); - - // Create the user, if needed - const federatedUser = FederatedUser.loadOrCreate(localPeerDomain, user); - const localUser = federatedUser.create(); - - // Callback management - Federation.peerClient.addCallbackToSkip('afterAddedToRoom', federatedUser.getFederationId()); - - // Add the user to the room - addUserToRoom(federatedRoom.room._id, localUser, localInviter, false); - - // Load federated users - federatedRoom.loadUsers(); - - // Refresh room's federation - federatedRoom.refreshFederation(); - - // Refresh federation peers - FederationPeers.refreshPeers(localPeerDomain); - } - - handleUserLeftEvent(e) { - this.log('handleUserLeftEvent'); - - const { peer: { domain: localPeerDomain } } = this.config; - - const { payload: { federated_room_id, federated_user_id } } = e; - - // Load the federated room - const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id); - - // Load the user who left - const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id); - const localUser = federatedUser.getLocalUser(); - - // Callback management - Federation.peerClient.addCallbackToSkip('beforeLeaveRoom', federatedUser.getFederationId()); - - // Remove the user from the room - removeUserFromRoom(federatedRoom.room._id, localUser); - - // Load federated users - federatedRoom.loadUsers(); - - // Refresh room's federation - federatedRoom.refreshFederation(); - - // Refresh federation peers - FederationPeers.refreshPeers(localPeerDomain); - } - - handleUserRemovedEvent(e) { - this.log('handleUserRemovedEvent'); - - const { peer: { domain: localPeerDomain } } = this.config; - - const { payload: { federated_room_id, federated_user_id, federated_removed_by_user_id } } = e; - - // Load the federated room - const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id); - - // Load the user who left - const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id); - const localUser = federatedUser.getLocalUser(); - - // Load the user who removed - const federatedUserWhoRemoved = FederatedUser.loadByFederationId(localPeerDomain, federated_removed_by_user_id); - const localUserWhoRemoved = federatedUserWhoRemoved.getLocalUser(); - - // Callback management - Federation.peerClient.addCallbackToSkip('beforeRemoveFromRoom', federatedUser.getFederationId()); - - // Remove the user from the room - removeUserFromRoom(federatedRoom.room._id, localUser, { byUser: localUserWhoRemoved }); - - // Load federated users - federatedRoom.loadUsers(); - - // Refresh room's federation - federatedRoom.refreshFederation(); - - // Refresh federation peers - FederationPeers.refreshPeers(localPeerDomain); - } - - handleUserMutedEvent(e) { - this.log('handleUserMutedEvent'); - - const { peer: { domain: localPeerDomain } } = this.config; - - const { payload: { federated_room_id, federated_user_id } } = e; - // const { payload: { federated_room_id, federated_user_id, federated_muted_by_user_id } } = e; - - // Load the federated room - const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id); - - // Load the user who left - const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id); - const localUser = federatedUser.getLocalUser(); - - // // Load the user who muted - // const federatedUserWhoMuted = FederatedUser.loadByFederationId(localPeerDomain, federated_muted_by_user_id); - // const localUserWhoMuted = federatedUserWhoMuted.getLocalUser(); - - // Mute user - Rooms.muteUsernameByRoomId(federatedRoom.room._id, localUser.username); - - // TODO: should we create a message? - } - - handleUserUnmutedEvent(e) { - this.log('handleUserUnmutedEvent'); - - const { peer: { domain: localPeerDomain } } = this.config; - - const { payload: { federated_room_id, federated_user_id } } = e; - // const { payload: { federated_room_id, federated_user_id, federated_unmuted_by_user_id } } = e; - - // Load the federated room - const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id); - - // Load the user who left - const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id); - const localUser = federatedUser.getLocalUser(); - - // // Load the user who muted - // const federatedUserWhoUnmuted = FederatedUser.loadByFederationId(localPeerDomain, federated_unmuted_by_user_id); - // const localUserWhoUnmuted = federatedUserWhoUnmuted.getLocalUser(); - - // Unmute user - Rooms.unmuteUsernameByRoomId(federatedRoom.room._id, localUser.username); - - // TODO: should we create a message? - } - - handleMessageCreatedEvent(e) { - this.log('handleMessageCreatedEvent'); - - const { peer: { domain: localPeerDomain } } = this.config; - - const { payload: { message } } = e; - - // Load the federated message - const federatedMessage = new FederatedMessage(localPeerDomain, message); - - // Callback management - Federation.peerClient.addCallbackToSkip('afterSaveMessage', federatedMessage.getFederationId()); - - // Create the federated message - federatedMessage.create(); - } - - handleMessageUpdatedEvent(e) { - this.log('handleMessageUpdatedEvent'); - - const { peer: { domain: localPeerDomain } } = this.config; - - const { payload: { message, federated_user_id } } = e; - - // Load the federated message - const federatedMessage = new FederatedMessage(localPeerDomain, message); - - // Load the federated user - const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id); - - // Callback management - Federation.peerClient.addCallbackToSkip('afterSaveMessage', federatedMessage.getFederationId()); - - // Update the federated message - federatedMessage.update(federatedUser); - } - - handleMessageDeletedEvent(e) { - this.log('handleMessageDeletedEvent'); - - const { peer: { domain: localPeerDomain } } = this.config; - - const { payload: { federated_message_id } } = e; - - const federatedMessage = FederatedMessage.loadByFederationId(localPeerDomain, federated_message_id); - - // Load the federated message - const localMessage = federatedMessage.getLocalMessage(); - - // Load the author - const localAuthor = federatedMessage.federatedAuthor.getLocalUser(); - - // Callback management - Federation.peerClient.addCallbackToSkip('afterDeleteMessage', federatedMessage.getFederationId()); - - // Create the federated message - deleteMessage(localMessage, localAuthor); - } - - handleMessagesReadEvent(e) { - this.log('handleMessagesReadEvent'); - - const { peer: { domain: localPeerDomain } } = this.config; - - const { payload: { federated_room_id, federated_user_id } } = e; - - // Load the federated room - const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id); - - Federation.peerClient.addCallbackToSkip('afterReadMessages', federatedRoom.getFederationId()); - - // Load the user who left - const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id); - const localUser = federatedUser.getLocalUser(); - - // Mark the messages as read - // TODO: move below calls to an exported function - const userSubscription = Subscriptions.findOneByRoomIdAndUserId(federatedRoom.room._id, localUser._id, { fields: { ls: 1 } }); - Subscriptions.setAsReadByRoomIdAndUserId(federatedRoom.room._id, localUser._id); - - callbacks.run('afterReadMessages', federatedRoom.room._id, { userId: localUser._id, lastSeen: userSubscription.ls }); - } - - handleMessagesSetReactionEvent(e) { - this.log('handleMessagesSetReactionEvent'); - - const { peer: { domain: localPeerDomain } } = this.config; - - const { payload: { federated_room_id, federated_message_id, federated_user_id, reaction, shouldReact } } = e; - - // Load the federated room - const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id); - const localRoom = federatedRoom.getLocalRoom(); - - // Load the user who reacted - const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id); - const localUser = federatedUser.getLocalUser(); - - // Load the message - const federatedMessage = FederatedMessage.loadByFederationId(localPeerDomain, federated_message_id); - const localMessage = federatedMessage.getLocalMessage(); - - // Callback management - Federation.peerClient.addCallbackToSkip('afterSetReaction', federatedMessage.getFederationId()); - - // Set message reaction - setReaction(localRoom, localUser, localMessage, reaction, shouldReact); - } - - handleMessagesUnsetReactionEvent(e) { - this.log('handleMessagesUnsetReactionEvent'); - - const { peer: { domain: localPeerDomain } } = this.config; - - const { payload: { federated_room_id, federated_message_id, federated_user_id, reaction, shouldReact } } = e; - - // Load the federated room - const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id); - const localRoom = federatedRoom.getLocalRoom(); - - // Load the user who reacted - const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id); - const localUser = federatedUser.getLocalUser(); - - // Load the message - const federatedMessage = FederatedMessage.loadByFederationId(localPeerDomain, federated_message_id); - const localMessage = federatedMessage.getLocalMessage(); - - // Callback management - Federation.peerClient.addCallbackToSkip('afterUnsetReaction', federatedMessage.getFederationId()); - - // Unset message reaction - setReaction(localRoom, localUser, localMessage, reaction, shouldReact); - } -} diff --git a/app/federation/server/PeerServer/index.js b/app/federation/server/PeerServer/index.js deleted file mode 100644 index e1da97c3327..00000000000 --- a/app/federation/server/PeerServer/index.js +++ /dev/null @@ -1,6 +0,0 @@ -// Setup routes -import './routes/events'; -import './routes/uploads'; -import './routes/users'; - -export { PeerServer } from './PeerServer'; diff --git a/app/federation/server/PeerServer/routes/events.js b/app/federation/server/PeerServer/routes/events.js deleted file mode 100644 index 45153c80bd7..00000000000 --- a/app/federation/server/PeerServer/routes/events.js +++ /dev/null @@ -1,115 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { API } from '../../../../api'; -import { FederationKeys } from '../../../../models'; -import { Federation } from '../..'; - -API.v1.addRoute('federation.events', { authRequired: false }, { - post() { - if (!Federation.peerServer.enabled) { - return API.v1.failure('Not found'); - } - - if (!this.bodyParams.payload) { - return API.v1.failure('Payload was not sent'); - } - - if (!this.request.headers['x-federation-domain']) { - return API.v1.failure('Cannot handle that request'); - } - - const remotePeerDomain = this.request.headers['x-federation-domain']; - - const peer = Federation.peerDNS.searchPeer(remotePeerDomain); - - if (!peer) { - return API.v1.failure('Could not find valid peer'); - } - - const payloadBuffer = Buffer.from(this.bodyParams.payload.data); - - let payload; - - // Decrypt with the peer's public key - try { - payload = FederationKeys.loadKey(peer.public_key, 'public').decryptPublic(payloadBuffer); - - // Decrypt with the local private key - payload = Federation.privateKey.decrypt(payload); - } catch (err) { - throw new Meteor.Error('error-decrypt', 'Could not decrypt'); - } - - // Get the event - const { event: e } = JSON.parse(payload.toString()); - - if (!e) { - return API.v1.failure('Event was not sent'); - } - - Federation.peerServer.log(`Received event:${ e.t }`); - - try { - switch (e.t) { - case 'png': - // This is a ping so we should do nothing, just respond with success - break; - case 'drc': - Federation.peerServer.handleDirectRoomCreatedEvent(e); - break; - case 'roc': - Federation.peerServer.handleRoomCreatedEvent(e); - break; - case 'usj': - Federation.peerServer.handleUserJoinedEvent(e); - break; - case 'usa': - Federation.peerServer.handleUserAddedEvent(e); - break; - case 'usl': - Federation.peerServer.handleUserLeftEvent(e); - break; - case 'usr': - Federation.peerServer.handleUserRemovedEvent(e); - break; - case 'usm': - Federation.peerServer.handleUserMutedEvent(e); - break; - case 'usu': - Federation.peerServer.handleUserUnmutedEvent(e); - break; - case 'msc': - Federation.peerServer.handleMessageCreatedEvent(e); - break; - case 'msu': - Federation.peerServer.handleMessageUpdatedEvent(e); - break; - case 'msd': - Federation.peerServer.handleMessageDeletedEvent(e); - break; - case 'msr': - Federation.peerServer.handleMessagesReadEvent(e); - break; - case 'mrs': - Federation.peerServer.handleMessagesSetReactionEvent(e); - break; - case 'mru': - Federation.peerServer.handleMessagesUnsetReactionEvent(e); - break; - default: - throw new Error(`Invalid event:${ e.t }`); - } - - Federation.peerServer.log('Success, responding...'); - - // Respond - return API.v1.success(); - } catch (err) { - console.log(err); - - Federation.peerServer.error(`Error handling event:${ e.t } - ${ err.toString() }`); - - return API.v1.failure(`Error handling event:${ e.t } - ${ err.toString() }`, err.error || 'unknown-error'); - } - }, -}); diff --git a/app/federation/server/PeerServer/routes/users.js b/app/federation/server/PeerServer/routes/users.js deleted file mode 100644 index 89006ad583a..00000000000 --- a/app/federation/server/PeerServer/routes/users.js +++ /dev/null @@ -1,48 +0,0 @@ -import { API } from '../../../../api'; -import { Users } from '../../../../models'; -import { FederatedUser } from '../../federatedResources'; -import { Federation } from '../..'; - -API.v1.addRoute('federation.users', { authRequired: false }, { - get() { - if (!Federation.peerServer.enabled) { - return API.v1.failure('Not found'); - } - - const { peer: { domain: localPeerDomain } } = Federation.peerServer.config; - - const { username, domain, usernameOnly } = this.requestParams(); - - const email = `${ username }@${ domain }`; - - Federation.peerServer.log(`[users] Trying to find user by username:${ username } and email:${ email }`); - - const query = { - type: 'user', - }; - - if (usernameOnly === 'true') { - query.username = username; - } else { - query.$or = [ - { name: username }, - { username }, - { 'emails.address': email }, - ]; - } - - const users = Users.find(query, { fields: { services: 0, roles: 0 } }).fetch(); - - if (!users.length) { - return API.v1.failure('There is no such user in this server'); - } - - const federatedUsers = []; - - for (const user of users) { - federatedUsers.push(new FederatedUser(localPeerDomain, user)); - } - - return API.v1.success({ federatedUsers }); - }, -}); diff --git a/app/federation/server/config.js b/app/federation/server/config.js deleted file mode 100644 index b8f1f2b1228..00000000000 --- a/app/federation/server/config.js +++ /dev/null @@ -1,74 +0,0 @@ -import mem from 'mem'; - -import { getWorkspaceAccessToken } from '../../cloud/server'; -import { FederationKeys } from '../../models/server'; -import { settings } from '../../settings/server'; -import * as SettingsUpdater from './settingsUpdater'; -import { logger } from './logger'; - -const defaultConfig = { - hub: { - active: null, - url: null, - }, - peer: { - uniqueId: null, - domain: null, - url: null, - public_key: null, - }, - cloud: { - token: null, - }, -}; - -const getConfigLocal = () => { - const _enabled = settings.get('FEDERATION_Enabled'); - - if (!_enabled) { return defaultConfig; } - - // If it is enabled, check if the settings are there - const _uniqueId = settings.get('FEDERATION_Unique_Id'); - const _domain = settings.get('FEDERATION_Domain'); - const _discoveryMethod = settings.get('FEDERATION_Discovery_Method'); - const _hubUrl = settings.get('FEDERATION_Hub_URL'); - const _peerUrl = settings.get('Site_Url'); - - if (!_domain || !_discoveryMethod || !_hubUrl || !_peerUrl) { - SettingsUpdater.updateStatus('Could not enable, settings are not fully set'); - - logger.setup.error('Could not enable Federation, settings are not fully set'); - - return defaultConfig; - } - - logger.setup.info('Updating settings...'); - - // Normalize the config values - return { - hub: { - active: _discoveryMethod === 'hub', - url: _hubUrl.replace(/\/+$/, ''), - }, - peer: { - uniqueId: _uniqueId, - domain: _domain.replace('@', '').trim(), - url: _peerUrl.replace(/\/+$/, ''), - public_key: FederationKeys.getPublicKeyString(), - }, - cloud: { - token: getWorkspaceAccessToken(), - }, - }; -}; - -export const getConfig = mem(getConfigLocal); - -const updateValue = () => mem.clear(getConfig); - -settings.get('FEDERATION_Enabled', updateValue); -settings.get('FEDERATION_Unique_Id', updateValue); -settings.get('FEDERATION_Domain', updateValue); -settings.get('FEDERATION_Status', updateValue); -settings.get('FEDERATION_Discovery_Method', updateValue); -settings.get('FEDERATION_Hub_URL', updateValue); diff --git a/app/federation/server/endpoints/dispatch.js b/app/federation/server/endpoints/dispatch.js new file mode 100644 index 00000000000..b0693b2ab43 --- /dev/null +++ b/app/federation/server/endpoints/dispatch.js @@ -0,0 +1,419 @@ +import { Meteor } from 'meteor/meteor'; +import { EJSON } from 'meteor/ejson'; + +import { API } from '../../../api/server'; +import { logger } from '../lib/logger'; +import { contextDefinitions, eventTypes } from '../../../models/server/models/FederationEvents'; +import { + FederationRoomEvents, FederationServers, + Messages, + Rooms, + Subscriptions, + Users, +} from '../../../models/server'; +import { normalizers } from '../normalizers'; +import { deleteRoom } from '../../../lib/server/functions'; +import { Notifications } from '../../../notifications/server'; +import { FileUpload } from '../../../file-upload'; +import { getFederationDomain } from '../lib/getFederationDomain'; +import { decryptIfNeeded } from '../lib/crypt'; +import { isFederationEnabled } from '../lib/isFederationEnabled'; +import { getUpload, requestEventsFromLatest } from '../handler'; + +API.v1.addRoute('federation.events.dispatch', { authRequired: false }, { + async post() { + if (!isFederationEnabled()) { + return API.v1.failure('Federation not enabled'); + } + + // + // Decrypt the payload if needed + let payload; + + try { + payload = decryptIfNeeded(this.request, this.bodyParams); + } catch (err) { + return API.v1.failure('Could not decrypt payload'); + } + + // + // Convert from EJSON + const { events } = EJSON.fromJSONValue(payload); + + logger.server.debug(`federation.events.dispatch => events=${ events.map((e) => JSON.stringify(e, null, 2)) }`); + + // Loop over received events + for (const event of events) { + /* eslint-disable no-await-in-loop */ + + let eventResult; + + switch (event.type) { + // + // PING + // + case eventTypes.PING: + eventResult = { + success: true, + }; + break; + + // + // GENESIS + // + case eventTypes.GENESIS: + switch (event.data.contextType) { + case contextDefinitions.ROOM.type: + 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 = Rooms.findOne({ _id: room._id }); + + if (persistedRoom) { + // Update the federation + Rooms.update({ _id: persistedRoom._id }, { $set: { federation: room.federation } }); + } else { + // Denormalize room + const denormalizedRoom = normalizers.denormalizeRoom(room); + + // Create the room + Rooms.insert(denormalizedRoom); + } + } + break; + } + break; + + // + // ROOM_DELETE + // + case eventTypes.ROOM_DELETE: + const { data: { roomId } } = event; + + // Check if room exists + const persistedRoom = Rooms.findOne({ _id: roomId }); + + if (persistedRoom) { + // Delete the room + deleteRoom(roomId); + } + + // Remove all room events + await FederationRoomEvents.removeRoomEvents(roomId); + + eventResult = { + success: true, + }; + + break; + + // + // ROOM_ADD_USER + // + case eventTypes.ROOM_ADD_USER: + eventResult = await FederationRoomEvents.addEvent(event.context, event); + + // 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 = Users.findOne({ _id: user._id }); + + if (persistedUser) { + // Update the federation + Users.update({ _id: persistedUser._id }, { $set: { federation: user.federation } }); + } else { + // Denormalize user + const denormalizedUser = normalizers.denormalizeUser(user); + + // Create the user + Users.insert(denormalizedUser); + } + + // Check if subscription exists + const persistedSubscription = Subscriptions.findOne({ _id: subscription._id }); + + if (persistedSubscription) { + // Update the federation + Subscriptions.update({ _id: persistedSubscription._id }, { $set: { federation: subscription.federation } }); + } else { + // Denormalize subscription + const denormalizedSubscription = normalizers.denormalizeSubscription(subscription); + + // Create the subscription + Subscriptions.insert(denormalizedSubscription); + } + + // Refresh the servers list + FederationServers.refreshServers(); + + // Update the room's federation property + Rooms.update({ _id: roomId }, { $set: { 'federation.domains': domainsAfterAdd } }); + } + break; + + // + // ROOM_REMOVE_USER + // + case eventTypes.ROOM_REMOVE_USER: + 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 + Subscriptions.removeByRoomIdAndUserId(roomId, user._id); + + // Refresh the servers list + FederationServers.refreshServers(); + + // Update the room's federation property + Rooms.update({ _id: roomId }, { $set: { 'federation.domains': domainsAfterRemoval } }); + } + break; + + // + // ROOM_MESSAGE + // + case eventTypes.ROOM_MESSAGE: + 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 = Messages.findOne({ _id: message._id }); + + if (persistedMessage) { + // Update the federation + Messages.update({ _id: persistedMessage._id }, { $set: { federation: message.federation } }); + } else { + // Update the subscription open status + Subscriptions.update({ rid: message.rid, name: message.u.username }, { $set: { open: true, alert: true } }); + + // Denormalize user + const denormalizedMessage = normalizers.denormalizeMessage(message); + + // Is there a file? + if (denormalizedMessage.file) { + const fileStore = FileUpload.getStore('Uploads'); + + const { federation: { origin } } = denormalizedMessage; + + const { upload, buffer } = 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, + }; + + Meteor.runAsUser(upload.userId, () => Meteor.wrapAsync(fileStore.insert.bind(fileStore))(upload, buffer)); + + // Update the message's file + denormalizedMessage.file._id = upload._id; + + // Update the message's attachments + for (const attachment of denormalizedMessage.attachments) { + attachment.title_link = attachment.title_link.replace(oldUploadId, upload._id); + attachment.image_url = attachment.image_url.replace(oldUploadId, upload._id); + } + } + + // Create the message + Messages.insert(denormalizedMessage); + } + } + break; + + // + // ROOM_EDIT_MESSAGE + // + case eventTypes.ROOM_EDIT_MESSAGE: + 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 = Messages.findOne({ _id: message._id }); + + if (!persistedMessage) { + eventResult.success = false; + eventResult.reason = 'missingMessageToEdit'; + } else { + // Update the message + Messages.update({ _id: persistedMessage._id }, { $set: { msg: message.msg, federation: message.federation } }); + } + } + break; + + // + // ROOM_DELETE_MESSAGE + // + case eventTypes.ROOM_DELETE_MESSAGE: + 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 + Messages.removeById(messageId); + + // Notify the room + Notifications.notifyRoom(roomId, 'deleteMessage', { _id: messageId }); + } + break; + + // + // ROOM_SET_MESSAGE_REACTION + // + case eventTypes.ROOM_SET_MESSAGE_REACTION: + 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 = 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 + Messages.update({ _id: messageId }, { $set: { [`reactions.${ reaction }`]: reactionObj } }); + } + break; + + // + // ROOM_UNSET_MESSAGE_REACTION + // + case eventTypes.ROOM_UNSET_MESSAGE_REACTION: + 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 = 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]) { + continue; + } + + 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) { + continue; + } + + // 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) { + Messages.update({ _id: messageId }, { $unset: { [`reactions.${ reaction }`]: 1 } }); + } else { + // Otherwise, update the property + Messages.update({ _id: messageId }, { $set: { [`reactions.${ reaction }`]: reactionObj } }); + } + } + break; + + // + // ROOM_MUTE_USER + // + case eventTypes.ROOM_MUTE_USER: + 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 + Rooms.muteUsernameByRoomId(roomId, denormalizedUser.username); + } + break; + + // + // ROOM_UNMUTE_USER + // + case eventTypes.ROOM_UNMUTE_USER: + 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 + Rooms.unmuteUsernameByRoomId(roomId, denormalizedUser.username); + } + break; + + // + // Could not find event + // + default: + continue; + } + + // If there was an error handling the event, take action + if (!eventResult.success) { + logger.server.debug(`federation.events.dispatch => Event has missing parents -> event=${ JSON.stringify(event, null, 2) }`); + + requestEventsFromLatest(event.origin, getFederationDomain(), contextDefinitions.defineType(event), event.context, eventResult.latestEventIds); + + // And stop handling the events + break; + } + + /* eslint-enable no-await-in-loop */ + } + + // Respond + return API.v1.success(); + }, +}); diff --git a/app/federation/server/endpoints/index.js b/app/federation/server/endpoints/index.js new file mode 100644 index 00000000000..a8a11611b82 --- /dev/null +++ b/app/federation/server/endpoints/index.js @@ -0,0 +1,4 @@ +import './dispatch'; +import './requestFromLatest'; +import './uploads'; +import './users'; diff --git a/app/federation/server/endpoints/requestFromLatest.js b/app/federation/server/endpoints/requestFromLatest.js new file mode 100644 index 00000000000..87917880229 --- /dev/null +++ b/app/federation/server/endpoints/requestFromLatest.js @@ -0,0 +1,61 @@ +import { EJSON } from 'meteor/ejson'; + +import { API } from '../../../api/server'; +import { logger } from '../lib/logger'; +import { FederationRoomEvents } from '../../../models/server'; +import { decryptIfNeeded } from '../lib/crypt'; +import { isFederationEnabled } from '../lib/isFederationEnabled'; +import { dispatchEvents } from '../handler'; + +API.v1.addRoute('federation.events.requestFromLatest', { authRequired: false }, { + async post() { + if (!isFederationEnabled()) { + return API.v1.failure('Federation not enabled'); + } + + // + // Decrypt the payload if needed + let payload; + + try { + payload = decryptIfNeeded(this.request, this.bodyParams); + } catch (err) { + return API.v1.failure('Could not decrypt payload'); + } + + const { fromDomain, contextType, contextQuery, latestEventIds } = EJSON.fromJSONValue(payload); + + logger.server.debug(`federation.events.requestFromLatest => contextType=${ contextType } contextQuery=${ JSON.stringify(contextQuery, null, 2) } latestEventIds=${ latestEventIds.join(', ') }`); + + 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 = EventsModel.findOne({ _id: { $in: latestEventIds } }, { $sort: { timestamp: 1 } }); + + if (!oldestEvent) { + return; + } + + // Get all the missing events on this context, after the oldest one + missingEvents = EventsModel.find({ _id: { $nin: latestEventIds }, context: contextQuery, timestamp: { $gte: oldestEvent.timestamp } }, { sort: { timestamp: 1 } }).fetch(); + } else { + // If there are no latest events, send all of them + missingEvents = EventsModel.find({ context: contextQuery }, { sort: { timestamp: 1 } }).fetch(); + } + + // Dispatch all the events, on the same request + dispatchEvents([fromDomain], missingEvents); + + return API.v1.success(); + }, +}); diff --git a/app/federation/server/PeerServer/routes/uploads.js b/app/federation/server/endpoints/uploads.js similarity index 61% rename from app/federation/server/PeerServer/routes/uploads.js rename to app/federation/server/endpoints/uploads.js index ec24f75ca2b..ca8b81a9208 100644 --- a/app/federation/server/PeerServer/routes/uploads.js +++ b/app/federation/server/endpoints/uploads.js @@ -1,14 +1,14 @@ import { Meteor } from 'meteor/meteor'; -import { API } from '../../../../api'; -import { Uploads } from '../../../../models'; -import { FileUpload } from '../../../../file-upload'; -import { Federation } from '../..'; +import { API } from '../../../api/server'; +import { Uploads } from '../../../models/server'; +import { FileUpload } from '../../../file-upload/server'; +import { isFederationEnabled } from '../lib/isFederationEnabled'; API.v1.addRoute('federation.uploads', { authRequired: false }, { get() { - if (!Federation.peerServer.enabled) { - return API.v1.failure('Not found'); + if (!isFederationEnabled()) { + return API.v1.failure('Federation not enabled'); } const { upload_id } = this.requestParams(); diff --git a/app/federation/server/endpoints/users.js b/app/federation/server/endpoints/users.js new file mode 100644 index 00000000000..d74cdae8324 --- /dev/null +++ b/app/federation/server/endpoints/users.js @@ -0,0 +1,57 @@ +import { API } from '../../../api/server'; +import { Users } from '../../../models/server'; +import { normalizers } from '../normalizers'; +import { logger } from '../lib/logger'; +import { isFederationEnabled } from '../lib/isFederationEnabled'; + +const userFields = { _id: 1, username: 1, type: 1, emails: 1, name: 1 }; + +API.v1.addRoute('federation.users.search', { authRequired: false }, { + get() { + if (!isFederationEnabled()) { + return API.v1.failure('Federation not enabled'); + } + + const { username, domain } = this.requestParams(); + + logger.server.debug(`federation.users.search => username=${ username } domain=${ domain }`); + + const query = { + type: 'user', + $or: [ + { name: username }, + { username }, + { 'emails.address': `${ username }@${ domain }` }, + ], + }; + + let users = Users.find(query, { fields: userFields }).fetch(); + + users = normalizers.normalizeAllUsers(users); + + return API.v1.success({ users }); + }, +}); + +API.v1.addRoute('federation.users.getByUsername', { authRequired: false }, { + get() { + if (!isFederationEnabled()) { + return API.v1.failure('Federation not enabled'); + } + + const { username } = this.requestParams(); + + logger.server.debug(`federation.users.getByUsername => username=${ username }`); + + const query = { + type: 'user', + username, + }; + + let user = Users.findOne(query, { fields: userFields }); + + user = normalizers.normalizeUser(user); + + return API.v1.success({ user }); + }, +}); diff --git a/app/federation/server/federatedResources/FederatedMessage.js b/app/federation/server/federatedResources/FederatedMessage.js deleted file mode 100644 index 92d98a0ada2..00000000000 --- a/app/federation/server/federatedResources/FederatedMessage.js +++ /dev/null @@ -1,263 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { sendMessage, updateMessage } from '../../../lib'; -import { Messages, Rooms, Users } from '../../../models'; -import { FileUpload } from '../../../file-upload'; -import { FederatedResource } from './FederatedResource'; -import { FederatedRoom } from './FederatedRoom'; -import { FederatedUser } from './FederatedUser'; - -import { Federation } from '..'; - -export class FederatedMessage extends FederatedResource { - constructor(localPeerIdentifier, message) { - super('message'); - - if (!message) { - throw new Error('message param cannot be empty'); - } - - // Set local peer identifier to local object - this.localPeerIdentifier = localPeerIdentifier; - - // Make sure room dates are correct - message.ts = new Date(message.ts); - message._updatedAt = new Date(message._updatedAt); - - // Set the message author - if (message.u.federation) { - this.federatedAuthor = FederatedUser.loadByFederationId(localPeerIdentifier, message.u.federation._id); - } else { - const author = Users.findOneById(message.u._id); - this.federatedAuthor = new FederatedUser(localPeerIdentifier, author); - } - - message.u = { - username: this.federatedAuthor.user.username, - federation: { - _id: this.federatedAuthor.user.federation._id, - }, - }; - - // Set the room - const room = Rooms.findOneById(message.rid); - - // Prepare the federation property - if (!message.federation) { - const federation = { - _id: message._id, - peer: localPeerIdentifier, - roomId: room.federation._id, - }; - - // Prepare the user - message.federation = federation; - - // Update the user - Messages.update(message._id, { $set: { federation } }); - - // Prepare mentions - for (const mention of message.mentions) { - mention.federation = mention.federation || {}; - - if (mention.username.indexOf('@') === -1) { - mention.federation.peer = localPeerIdentifier; - } else { - const [username, peer] = mention.username.split('@'); - - mention.username = username; - mention.federation.peer = peer; - } - } - - // Prepare channels - for (const channel of message.channels) { - channel.federation = channel.federation || {}; - - if (channel.name.indexOf('@') === -1) { - channel.federation.peer = localPeerIdentifier; - } else { - channel.name = channel.name.split('@')[0]; - channel.federation.peer = channel.name.split('@')[1]; - } - } - } - - // Set message property - this.message = message; - } - - getFederationId() { - return this.message.federation._id; - } - - getMessage() { - return this.message; - } - - getLocalMessage() { - this.log('getLocalMessage'); - - const { localPeerIdentifier, message } = this; - - const localMessage = Object.assign({}, message); - - // Make sure `u` is correct - if (!this.federatedAuthor) { - throw new Error('Author does not exist'); - } - - const localAuthor = this.federatedAuthor.getLocalUser(); - - localMessage.u = { - _id: localAuthor._id, - username: localAuthor.username, - }; - - // Make sure `rid` is correct - const federatedRoom = FederatedRoom.loadByFederationId(localPeerIdentifier, message.federation.roomId); - - if (!federatedRoom) { - throw new Error('Room does not exist'); - } - - const localRoom = federatedRoom.getLocalRoom(); - - localMessage.rid = localRoom._id; - - return localMessage; - } - - create() { - this.log('create'); - - // Get the local message object - const localMessageObject = this.getLocalMessage(); - - // Grab the federation id - const { federation: { _id: federationId } } = localMessageObject; - - // Check if the message exists - let localMessage = Messages.findOne({ 'federation._id': federationId }); - - // Create if needed - if (!localMessage) { - delete localMessageObject._id; - - localMessage = localMessageObject; - - const localRoom = Rooms.findOneById(localMessage.rid); - - // Normalize mentions - for (const mention of localMessage.mentions) { - // Ignore if we are dealing with all, here or rocket.cat - if (['all', 'here', 'rocket.cat'].indexOf(mention.username) !== -1) { continue; } - - let usernameToReplace = ''; - - if (mention.federation.peer !== this.localPeerIdentifier) { - usernameToReplace = mention.username; - - mention.username = `${ mention.username }@${ mention.federation.peer }`; - } else { - usernameToReplace = `${ mention.username }@${ mention.federation.peer }`; - } - - localMessage.msg = localMessage.msg.split(usernameToReplace).join(mention.username); - } - - // Normalize channels - for (const channel of localMessage.channels) { - if (channel.federation.peer !== this.localPeerIdentifier) { - channel.name = `${ channel.name }@${ channel.federation.peer }`; - } - } - - // Is there a file? - if (localMessage.file) { - const fileStore = FileUpload.getStore('Uploads'); - - const { federation: { peer: identifier } } = localMessage; - - const { upload, buffer } = Federation.peerClient.getUpload({ identifier, localMessage }); - - const oldUploadId = upload._id; - - // Normalize upload - delete upload._id; - upload.rid = localMessage.rid; - upload.userId = localMessage.u._id; - upload.federation = { - _id: localMessage.file._id, - peer: identifier, - }; - - Meteor.runAsUser(upload.userId, () => Meteor.wrapAsync(fileStore.insert.bind(fileStore))(upload, buffer)); - - // Update the message's file - localMessage.file._id = upload._id; - - // Update the message's attachments - for (const attachment of localMessage.attachments) { - attachment.title_link = attachment.title_link.replace(oldUploadId, upload._id); - attachment.image_url = attachment.image_url.replace(oldUploadId, upload._id); - } - } - - // Create the message - const { _id } = sendMessage(localMessage.u, localMessage, localRoom, false); - - localMessage._id = _id; - } - - return localMessage; - } - - update(updatedByFederatedUser) { - this.log('update'); - - // Get the original message - const originalMessage = Messages.findOne({ 'federation._id': this.getFederationId() }); - - // Error if message does not exist - if (!originalMessage) { - throw new Error('Message does not exist'); - } - - // Get the local message object - const localMessage = this.getLocalMessage(); - - // Make sure the message has the correct _id - localMessage._id = originalMessage._id; - - // Get the user who updated - const user = updatedByFederatedUser.getLocalUser(); - - // Update the message - updateMessage(localMessage, user, originalMessage); - - return localMessage; - } -} - -FederatedMessage.loadByFederationId = function loadByFederationId(localPeerIdentifier, federationId) { - const localMessage = Messages.findOne({ 'federation._id': federationId }); - - if (!localMessage) { return; } - - return new FederatedMessage(localPeerIdentifier, localMessage); -}; - -FederatedMessage.loadOrCreate = function loadOrCreate(localPeerIdentifier, message) { - const { federation } = message; - - if (federation) { - const federatedMessage = FederatedMessage.loadByFederationId(localPeerIdentifier, federation._id); - - if (federatedMessage) { - return federatedMessage; - } - } - - return new FederatedMessage(localPeerIdentifier, message); -}; diff --git a/app/federation/server/federatedResources/FederatedResource.js b/app/federation/server/federatedResources/FederatedResource.js deleted file mode 100644 index 7ecdb9ec1cd..00000000000 --- a/app/federation/server/federatedResources/FederatedResource.js +++ /dev/null @@ -1,17 +0,0 @@ -import { logger } from '../logger'; - -export class FederatedResource { - constructor(name) { - this.resourceName = `federated-${ name }`; - - this.log('Creating federated resource'); - } - - log(message) { - FederatedResource.log(this.resourceName, message); - } -} - -FederatedResource.log = function log(name, message) { - logger.resource.info(`[${ name }] ${ message }`); -}; diff --git a/app/federation/server/federatedResources/FederatedRoom.js b/app/federation/server/federatedResources/FederatedRoom.js deleted file mode 100644 index 494edb1f96c..00000000000 --- a/app/federation/server/federatedResources/FederatedRoom.js +++ /dev/null @@ -1,275 +0,0 @@ -import { FederatedResource } from './FederatedResource'; -import { FederatedUser } from './FederatedUser'; -import { createRoom } from '../../../lib'; -import { Rooms, Subscriptions, Users } from '../../../models'; - - -export class FederatedRoom extends FederatedResource { - constructor(localPeerIdentifier, room, extras = {}) { - super('room'); - - if (!room) { - throw new Error('room param cannot be empty'); - } - - this.localPeerIdentifier = localPeerIdentifier; - - // Make sure room dates are correct - room.ts = new Date(room.ts); - room._updatedAt = new Date(room._updatedAt); - - // Set the name - if (room.t !== 'd' && room.name.indexOf('@') === -1) { - room.name = `${ room.name }@${ localPeerIdentifier }`; - } - - // Set the federated owner, if there is one - const { owner } = extras; - - if (owner) { - this.federatedOwner = FederatedUser.loadOrCreate(localPeerIdentifier, owner); - } else if (!owner && room.federation && room.federation.ownerId) { - this.federatedOwner = FederatedUser.loadByFederationId(localPeerIdentifier, room.federation.ownerId); - } - - // Set base federation - room.federation = room.federation || { - _id: room._id, - peer: localPeerIdentifier, - ownerId: this.federatedOwner ? this.federatedOwner.getFederationId() : null, - }; - - // Keep room's owner id - this.federationOwnerId = room.federation && room.federation.ownerId; - - // Set room property - this.room = room; - } - - getFederationId() { - return this.room.federation._id; - } - - getPeers() { - return this.room.federation.peers; - } - - getRoom() { - return this.room; - } - - getOwner() { - return this.federatedOwner ? this.federatedOwner.getUser() : null; - } - - getUsers() { - return this.federatedUsers.map((u) => u.getUser()); - } - - loadUsers() { - const { room } = this; - - // Get all room users - const users = FederatedRoom.loadRoomUsers(room); - - this.setUsers(users); - } - - setUsers(users) { - const { localPeerIdentifier } = this; - - // Initialize federatedUsers - this.federatedUsers = []; - - for (const user of users) { - const federatedUser = FederatedUser.loadOrCreate(localPeerIdentifier, user); - - // Set owner if it does not exist - if (!this.federatedOwner && user._id === this.federationOwnerId) { - this.federatedOwner = federatedUser; - this.room.federation.ownerId = this.federatedOwner.getFederationId(); - } - - // Keep the federated user - this.federatedUsers.push(federatedUser); - } - } - - refreshFederation() { - const { room } = this; - - // Prepare the federated users - let federation = { - peers: [], - users: [], - }; - - // Check all the peers - for (const federatedUser of this.federatedUsers) { - // Add federation data to the room - const { user: { federation: { _id, peer } } } = federatedUser; - - federation.peers.push(peer); - federation.users.push({ _id, peer }); - } - - federation.peers = [...new Set(federation.peers)]; - - federation = Object.assign(room.federation || {}, federation); - - // Prepare the room - room.federation = federation; - - // Update the room - Rooms.update(room._id, { $set: { federation } }); - } - - getLocalRoom() { - this.log('getLocalRoom'); - - const { localPeerIdentifier, room, room: { federation } } = this; - - const localRoom = Object.assign({}, room); - - if (federation.peer === localPeerIdentifier) { - if (localRoom.t !== 'd') { - localRoom.name = room.name.split('@')[0]; - } - } - - return localRoom; - } - - createUsers() { - this.log('createUsers'); - - const { federatedUsers } = this; - - // Create, if needed, all room's users - for (const federatedUser of federatedUsers) { - federatedUser.create(); - } - } - - create(alertAndOpen = false) { - this.log('create'); - - // Get the local room object (with or without suffixes) - const localRoomObject = this.getLocalRoom(); - - // Grab the federation id - const { federation: { _id: federationId } } = localRoomObject; - - // Check if the user exists - let localRoom = FederatedRoom.loadByFederationId(this.localPeerIdentifier, federationId); - - // Create if needed - if (!localRoom) { - delete localRoomObject._id; - - localRoom = localRoomObject; - - const { t: type, name, broadcast, customFields, federation, sysMes } = localRoom; - const { federatedOwner, federatedUsers } = this; - - // Get usernames for the owner and members - const ownerUsername = federatedOwner.user.username; - const members = []; - - if (type !== 'd') { - for (const federatedUser of federatedUsers) { - const localUser = federatedUser.getLocalUser(); - members.push(localUser.username); - } - } else { - for (const federatedUser of federatedUsers) { - const localUser = federatedUser.getLocalUser(); - members.push(localUser); - } - } - - // Is this a broadcast channel? Then mute everyone but the owner - let muted = []; - - if (broadcast) { - muted = members.filter((u) => u !== ownerUsername); - } - - // Set the extra data and create room options - let extraData = { - federation, - }; - - let createRoomOptions = { - subscriptionExtra: { - alert: alertAndOpen, - open: alertAndOpen, - }, - }; - - if (type !== 'd') { - extraData = Object.assign(extraData, { - broadcast, - customFields, - encrypted: false, // Always false for now - muted, - sysMes, - }); - - createRoomOptions = Object.assign(extraData, { - nameValidationRegex: '^[0-9a-zA-Z-_.@]+$', - subscriptionExtra: { - alert: true, - }, - }); - } - - // Create the room - // !!!! Forcing direct or private only, no public rooms for now - const { rid } = createRoom(type === 'd' ? type : 'p', name, ownerUsername, members, false, extraData, createRoomOptions); - - localRoom._id = rid; - } - - return localRoom; - } -} - -FederatedRoom.loadByFederationId = function _loadByFederationId(localPeerIdentifier, federationId) { - const localRoom = Rooms.findOne({ 'federation._id': federationId }); - - if (!localRoom) { return; } - - return new FederatedRoom(localPeerIdentifier, localRoom); -}; - -FederatedRoom.loadRoomUsers = function _loadRoomUsers(room) { - const subscriptions = Subscriptions.findByRoomIdWhenUsernameExists(room._id, { fields: { 'u._id': 1 } }).fetch(); - const userIds = subscriptions.map((s) => s.u._id); - return Users.findUsersWithUsernameByIds(userIds).fetch(); -}; - -FederatedRoom.isFederated = function _isFederated(localPeerIdentifier, room, options = {}) { - this.log('federated-room', `${ room._id } - isFederated?`); - - let isFederated = false; - - if (options.checkUsingUsers) { - // Get all room users - const users = FederatedRoom.loadRoomUsers(room); - - // Check all the users - for (const user of users) { - if (user.federation && user.federation.peer !== localPeerIdentifier) { - isFederated = true; - break; - } - } - } else { - isFederated = room.federation && room.federation.peers.length > 1; - } - - this.log('federated-room', `${ room._id } - isFederated? ${ isFederated ? 'yes' : 'no' }`); - - return isFederated; -}; diff --git a/app/federation/server/federatedResources/FederatedUser.js b/app/federation/server/federatedResources/FederatedUser.js deleted file mode 100644 index 5b21c5fd07f..00000000000 --- a/app/federation/server/federatedResources/FederatedUser.js +++ /dev/null @@ -1,125 +0,0 @@ -import { FederatedResource } from './FederatedResource'; -import { Users } from '../../../models'; - - -export class FederatedUser extends FederatedResource { - constructor(localPeerIdentifier, user) { - super('user'); - - if (!user) { - throw new Error('user param cannot be empty'); - } - - this.localPeerIdentifier = localPeerIdentifier; - - // Make sure all properties are normalized - // Prepare the federation property - if (!user.federation) { - const federation = { - _id: user._id, - peer: localPeerIdentifier, - }; - - // Prepare the user - user.federation = federation; - - // Update the user - Users.update(user._id, { $set: { federation } }); - } - - // Make sure user dates are correct - user.createdAt = new Date(user.createdAt); - user.lastLogin = new Date(user.lastLogin); - user._updatedAt = new Date(user._updatedAt); - - // Delete sensitive data as well - delete user.roles; - delete user.services; - - // Make sure some other properties are ready - user.name = user.name; - user.username = user.username.indexOf('@') === -1 ? `${ user.username }@${ user.federation.peer }` : user.username; - user.roles = ['user']; - user.status = 'online'; - user.statusConnection = 'online'; - user.type = 'user'; - - // Set user property - this.user = user; - } - - getFederationId() { - return this.user.federation._id; - } - - getUser() { - return this.user; - } - - getLocalUser() { - this.log('getLocalUser'); - - const { localPeerIdentifier, user, user: { federation } } = this; - - const localUser = Object.assign({}, user); - - if (federation.peer === localPeerIdentifier || user.username === 'rocket.cat') { - localUser.username = user.username.split('@')[0]; - localUser.name = user.name.split('@')[0]; - } - if (federation.peer !== localPeerIdentifier) { - localUser.isRemote = true; - } - - return localUser; - } - - create() { - this.log('create'); - - // Get the local user object (with or without suffixes) - const localUserObject = this.getLocalUser(); - - // Grab the federation id - const { federation: { _id: federationId } } = localUserObject; - - // Check if the user exists - let localUser = Users.findOne({ 'federation._id': federationId }); - - // Create if needed - if (!localUser) { - delete localUserObject._id; - - localUser = localUserObject; - - localUser._id = Users.create(localUserObject); - } - - // Update the id - this.user._id = localUser._id; - - return localUser; - } -} - -FederatedUser.loadByFederationId = function loadByFederationId(localPeerIdentifier, federationId) { - const localUser = Users.findOne({ 'federation._id': federationId }); - - if (!localUser) { return; } - - return new FederatedUser(localPeerIdentifier, localUser); -}; - -FederatedUser.loadOrCreate = function loadOrCreate(localPeerIdentifier, user) { - const { federation } = user; - - if (federation) { - const federatedUser = FederatedUser.loadByFederationId(localPeerIdentifier, federation._id); - - if (federatedUser) { - return federatedUser; - } - } - - return new FederatedUser(localPeerIdentifier, user); -}; diff --git a/app/federation/server/federatedResources/index.js b/app/federation/server/federatedResources/index.js deleted file mode 100644 index 90d98b351ca..00000000000 --- a/app/federation/server/federatedResources/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export { FederatedMessage } from './FederatedMessage'; -export { FederatedResource } from './FederatedResource'; -export { FederatedRoom } from './FederatedRoom'; -export { FederatedUser } from './FederatedUser'; diff --git a/app/federation/server/federation-settings.js b/app/federation/server/federation-settings.js deleted file mode 100644 index 385d077af5f..00000000000 --- a/app/federation/server/federation-settings.js +++ /dev/null @@ -1,76 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; -import { FederationKeys } from '../../models'; - -Meteor.startup(function() { - // const federationUniqueId = FederationKeys.getUniqueId(); - const federationPublicKey = FederationKeys.getPublicKeyString(); - - const defaultHubURL = process.env.NODE_ENV === 'development' ? 'http://localhost:8080' : 'https://hub.rocket.chat'; - - settings.addGroup('Federation', function() { - this.add('FEDERATION_Enabled', false, { - type: 'boolean', - i18nLabel: 'Enabled', - i18nDescription: 'FEDERATION_Enabled', - alert: 'FEDERATION_Enabled_Alert', - public: true, - }); - - this.add('FEDERATION_Status', '-', { - readonly: true, - type: 'string', - i18nLabel: 'FEDERATION_Status', - }); - - // this.add('FEDERATION_Unique_Id', federationUniqueId, { - // readonly: true, - // type: 'string', - // i18nLabel: 'FEDERATION_Unique_Id', - // i18nDescription: 'FEDERATION_Unique_Id_Description', - // }); - - this.add('FEDERATION_Domain', '', { - type: 'string', - i18nLabel: 'FEDERATION_Domain', - i18nDescription: 'FEDERATION_Domain_Description', - alert: 'FEDERATION_Domain_Alert', - disableReset: true, - }); - - this.add('FEDERATION_Public_Key', federationPublicKey, { - readonly: true, - type: 'string', - multiline: true, - i18nLabel: 'FEDERATION_Public_Key', - i18nDescription: 'FEDERATION_Public_Key_Description', - }); - - this.add('FEDERATION_Hub_URL', defaultHubURL, { - group: 'Federation Hub', - type: 'string', - i18nLabel: 'FEDERATION_Hub_URL', - i18nDescription: 'FEDERATION_Hub_URL_Description', - }); - - this.add('FEDERATION_Discovery_Method', 'dns', { - type: 'select', - values: [{ - key: 'dns', - i18nLabel: 'DNS', - }, { - key: 'hub', - i18nLabel: 'Hub', - }], - i18nLabel: 'FEDERATION_Discovery_Method', - i18nDescription: 'FEDERATION_Discovery_Method_Description', - public: true, - }); - - this.add('FEDERATION_Test_Setup', 'FEDERATION_Test_Setup', { - type: 'action', - actionText: 'FEDERATION_Test_Setup', - }); - }); -}); diff --git a/app/federation/server/functions/addUser.js b/app/federation/server/functions/addUser.js new file mode 100644 index 00000000000..eebd1656260 --- /dev/null +++ b/app/federation/server/functions/addUser.js @@ -0,0 +1,35 @@ +import { Meteor } from 'meteor/meteor'; + +import * as federationErrors from './errors'; +import { FederationServers, Users } from '../../../models/server'; +import { getUserByUsername } from '../handler'; + +export function addUser(query) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'addUser' }); + } + + const user = getUserByUsername(query); + + if (!user) { + throw federationErrors.userNotFound(query); + } + + let userId = user._id; + + try { + // Create the local user + userId = Users.create(user); + + // Refresh the servers list + 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/app/federation/server/functions/dashboard.js b/app/federation/server/functions/dashboard.js new file mode 100644 index 00000000000..137ef802c5d --- /dev/null +++ b/app/federation/server/functions/dashboard.js @@ -0,0 +1,44 @@ +import { Meteor } from 'meteor/meteor'; + +import { FederationServers, FederationRoomEvents, Users } from '../../../models/server'; + +export function getStatistics() { + const numberOfEvents = FederationRoomEvents.find().count(); + const numberOfFederatedUsers = Users.findRemote().count(); + const numberOfServers = FederationServers.find().count(); + + return { numberOfEvents, numberOfFederatedUsers, numberOfServers }; +} + +export function federationGetOverviewData() { + if (!Meteor.userId()) { + throw new Meteor.Error('not-authorized'); + } + + const { numberOfEvents, numberOfFederatedUsers, numberOfServers } = getStatistics(); + + return { + data: [{ + title: 'Number_of_events', + value: numberOfEvents, + }, { + title: 'Number_of_federated_users', + value: numberOfFederatedUsers, + }, { + title: 'Number_of_federated_servers', + value: numberOfServers, + }], + }; +} + +export function federationGetServers() { + if (!Meteor.userId()) { + throw new Meteor.Error('not-authorized'); + } + + const servers = FederationServers.find().fetch(); + + return { + data: servers, + }; +} diff --git a/app/federation/server/functions/errors.js b/app/federation/server/functions/errors.js new file mode 100644 index 00000000000..f4eb74e2b44 --- /dev/null +++ b/app/federation/server/functions/errors.js @@ -0,0 +1,13 @@ +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/app/federation/server/functions/helpers.js b/app/federation/server/functions/helpers.js new file mode 100644 index 00000000000..adbe2ac2162 --- /dev/null +++ b/app/federation/server/functions/helpers.js @@ -0,0 +1,62 @@ +import { Settings, Subscriptions, Users } from '../../../models/server'; + +export const getNameAndDomain = (fullyQualifiedName) => fullyQualifiedName.split('@'); +export const isFullyQualified = (name) => name.indexOf('@') !== -1; + +export function updateStatus(status) { + Settings.updateValueById('FEDERATION_Status', status); +} + +export function updateEnabled(enabled) { + Settings.updateValueById('FEDERATION_Enabled', enabled); +} + +export const isFederated = (resource) => !!resource.federation; + +export const hasExternalDomain = ({ federation }) => { + // same test as isFederated(room) + if (!federation) { + return false; + } + + return federation.domains + .some((domain) => domain !== federation.origin); +}; + +export const isLocalUser = ({ federation }, localDomain) => + !federation || federation.origin === localDomain; + +export const getFederatedRoomData = (room) => { + let hasFederatedUser = false; + + let users = null; + let subscriptions = null; + + if (room.t === 'd') { + // Check if there is a federated user on this room + hasFederatedUser = room.usernames.find((u) => u.indexOf('@') !== -1); + } else { + // Find all subscriptions of this room + subscriptions = Subscriptions.findByRoomIdWhenUsernameExists(room._id).fetch(); + 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 + users = Users.findUsersWithUsernameByIds(userIds).fetch(); + + // Check if there is a federated user on this room + hasFederatedUser = users.find((u) => u.username.indexOf('@') !== -1); + } + + return { + hasFederatedUser, + users, + subscriptions, + }; +}; diff --git a/app/federation/server/functions/searchUsers.js b/app/federation/server/functions/searchUsers.js new file mode 100644 index 00000000000..7d9e349b9fe --- /dev/null +++ b/app/federation/server/functions/searchUsers.js @@ -0,0 +1,19 @@ +import { Meteor } from 'meteor/meteor'; + +import * as federationErrors from './errors'; +import { normalizers } from '../normalizers'; +import { federationSearchUsers } from '../handler'; + +export function searchUsers(query) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'searchUsers' }); + } + + const users = federationSearchUsers(query); + + if (!users.length) { + throw federationErrors.userNotFound(query); + } + + return normalizers.denormalizeAllUsers(users); +} diff --git a/app/federation/server/handler/index.js b/app/federation/server/handler/index.js new file mode 100644 index 00000000000..e6bf2be85cb --- /dev/null +++ b/app/federation/server/handler/index.js @@ -0,0 +1,75 @@ +import qs from 'querystring'; + +import { disabled } from '../functions/errors'; +import { logger } from '../lib/logger'; +import { isFederationEnabled } from '../lib/isFederationEnabled'; +import { federationRequestToPeer } from '../lib/http'; + +export function federationSearchUsers(query) { + if (!isFederationEnabled()) { + throw disabled('client.searchUsers'); + } + + logger.client.debug(() => `searchUsers => query=${ query }`); + + const [username, peerDomain] = query.split('@'); + + const uri = `/api/v1/federation.users.search?${ qs.stringify({ username, domain: peerDomain }) }`; + + const { data: { users } } = federationRequestToPeer('GET', peerDomain, uri); + + return users; +} + +export function getUserByUsername(query) { + if (!isFederationEnabled()) { + throw disabled('client.searchUsers'); + } + + logger.client.debug(() => `getUserByUsername => query=${ query }`); + + const [username, peerDomain] = query.split('@'); + + const uri = `/api/v1/federation.users.getByUsername?${ qs.stringify({ username }) }`; + + const { data: { user } } = federationRequestToPeer('GET', peerDomain, uri); + + return user; +} + +export function requestEventsFromLatest(domain, fromDomain, contextType, contextQuery, latestEventIds) { + if (!isFederationEnabled()) { + throw disabled('client.requestEventsFromLatest'); + } + + logger.client.debug(() => `requestEventsFromLatest => domain=${ domain } contextType=${ contextType } contextQuery=${ JSON.stringify(contextQuery, null, 2) } latestEventIds=${ latestEventIds.join(', ') }`); + + const uri = '/api/v1/federation.events.requestFromLatest'; + + federationRequestToPeer('POST', domain, uri, { fromDomain, contextType, contextQuery, latestEventIds }); +} + + +export function dispatchEvents(domains, events) { + if (!isFederationEnabled()) { + throw disabled('client.dispatchEvents'); + } + + logger.client.debug(() => `dispatchEvents => domains=${ domains.join(', ') } events=${ events.map((e) => JSON.stringify(e, null, 2)) }`); + + const uri = '/api/v1/federation.events.dispatch'; + + for (const domain of domains) { + federationRequestToPeer('POST', domain, uri, { events }, { ignoreErrors: true }); + } +} + +export function dispatchEvent(domains, event) { + dispatchEvents(domains, [event]); +} + +export function getUpload(domain, fileId) { + const { data: { upload, buffer } } = federationRequestToPeer('GET', domain, `/api/v1/federation.uploads?${ qs.stringify({ upload_id: fileId }) }`); + + return { upload, buffer: Buffer.from(buffer) }; +} diff --git a/app/federation/server/hooks/afterAddedToRoom.js b/app/federation/server/hooks/afterAddedToRoom.js new file mode 100644 index 00000000000..14f50d2c083 --- /dev/null +++ b/app/federation/server/hooks/afterAddedToRoom.js @@ -0,0 +1,70 @@ +import { logger } from '../lib/logger'; +import { getFederatedRoomData, hasExternalDomain, isLocalUser } from '../functions/helpers'; +import { FederationRoomEvents, Subscriptions } from '../../../models/server'; +import { normalizers } from '../normalizers'; +import { doAfterCreateRoom } from './afterCreateRoom'; +import { getFederationDomain } from '../lib/getFederationDomain'; +import { dispatchEvent } from '../handler'; + +async function afterAddedToRoom(involvedUsers, room) { + const { user: addedUser } = involvedUsers; + + const localDomain = getFederationDomain(); + + if (!hasExternalDomain(room) && isLocalUser(addedUser, localDomain)) { + return involvedUsers; + } + + logger.client.debug(() => `afterAddedToRoom => involvedUsers=${ JSON.stringify(involvedUsers, null, 2) } room=${ JSON.stringify(room, null, 2) }`); + + // If there are not federated users on this room, ignore it + const { users, subscriptions } = getFederatedRoomData(room); + + // Load the subscription + const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, addedUser._id); + + try { + // + // 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.map((u) => u.federation.origin); + + // + // Create the user add event + // + + const normalizedSourceUser = 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 }); + + logger.client.error(() => `afterAddedToRoom => involvedUsers=${ JSON.stringify(involvedUsers, null, 2) } => Could not add user: ${ err }`); + } + + return involvedUsers; +} + +export const definition = { + hook: 'afterAddedToRoom', + callback: (roomOwner, room) => Promise.await(afterAddedToRoom(roomOwner, room)), + id: 'federation-after-added-to-room', +}; diff --git a/app/federation/server/hooks/afterCreateDirectRoom.js b/app/federation/server/hooks/afterCreateDirectRoom.js new file mode 100644 index 00000000000..78f1e303627 --- /dev/null +++ b/app/federation/server/hooks/afterCreateDirectRoom.js @@ -0,0 +1,75 @@ +import { logger } from '../lib/logger'; +import { FederationRoomEvents, Subscriptions } from '../../../models/server'; +import { normalizers } from '../normalizers'; +import { deleteRoom } from '../../../lib/server/functions'; +import { getFederationDomain } from '../lib/getFederationDomain'; +import { dispatchEvents } from '../handler'; +import { hasExternalDomain } from '../functions/helpers'; + +async function afterCreateDirectRoom(room, extras) { + logger.client.debug(() => `afterCreateDirectRoom => room=${ JSON.stringify(room, null, 2) } extras=${ JSON.stringify(extras, null, 2) }`); + + // If the room is federated, ignore + if (!hasExternalDomain(room)) { return room; } + + // Check if there is a federated user on this direct room + const hasFederatedUser = room.usernames.find((u) => u.indexOf('@') !== -1); + + // 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); + + // + // Source User + // + + // Add the source user to the room + const sourceUser = extras.from; + const normalizedSourceUser = normalizers.normalizeUser(sourceUser); + + const sourceSubscription = Subscriptions.findOne({ rid: normalizedRoom._id, 'u._id': normalizedSourceUser._id }); + const normalizedSourceSubscription = normalizers.normalizeSubscription(sourceSubscription); + + // Build the source user event + const sourceUserEvent = await FederationRoomEvents.createAddUserEvent(getFederationDomain(), normalizedRoom._id, normalizedSourceUser, normalizedSourceSubscription); + + // + // Target User + // + + // Add the target user to the room + const targetUser = extras.to; + const normalizedTargetUser = normalizers.normalizeUser(targetUser); + + const targetSubscription = Subscriptions.findOne({ rid: normalizedRoom._id, 'u._id': normalizedTargetUser._id }); + const normalizedTargetSubscription = normalizers.normalizeSubscription(targetSubscription); + + // Dispatch the target user event + const targetUserEvent = await FederationRoomEvents.createAddUserEvent(getFederationDomain(), normalizedRoom._id, normalizedTargetUser, normalizedTargetSubscription); + + // Dispatch the events + dispatchEvents(normalizedRoom.federation.domains, [genesisEvent, sourceUserEvent, targetUserEvent]); + } catch (err) { + Promise.await(deleteRoom(room._id)); + + logger.client.error(() => `afterCreateDirectRoom => room=${ JSON.stringify(room, null, 2) } => Could not create federated room: ${ err }`); + } + + return room; +} + +export const definition = { + hook: 'afterCreateDirectRoom', + callback: (room, extras) => Promise.await(afterCreateDirectRoom(room, extras)), + id: 'federation-after-create-direct-room', +}; diff --git a/app/federation/server/hooks/afterCreateRoom.js b/app/federation/server/hooks/afterCreateRoom.js new file mode 100644 index 00000000000..83043c0062e --- /dev/null +++ b/app/federation/server/hooks/afterCreateRoom.js @@ -0,0 +1,88 @@ +import { logger } from '../lib/logger'; +import { FederationRoomEvents, Subscriptions, Users } from '../../../models/server'; +import { normalizers } from '../normalizers'; +import { deleteRoom } from '../../../lib/server/functions'; +import { getFederationDomain } from '../lib/getFederationDomain'; +import { dispatchEvents } from '../handler'; + +export async function doAfterCreateRoom(room, users, subscriptions) { + const normalizedUsers = []; + + // + // Add user events + // + const addUserEvents = []; + + for (const user of users) { + /* eslint-disable no-await-in-loop */ + + const subscription = subscriptions[user._id]; + + const normalizedSourceUser = 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); + + // Ensure a genesis event for this room + const genesisEvent = await FederationRoomEvents.createGenesisEvent(getFederationDomain(), normalizedRoom); + + // Dispatch the events + 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 = Subscriptions.findByRoomIdWhenUsernameExists(room._id).fetch(); + 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 = Users.findUsersWithUsernameByIds(userIds).fetch(); + + // 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; } + + logger.client.debug(() => `afterCreateRoom => roomOwner=${ JSON.stringify(roomOwner, null, 2) } room=${ JSON.stringify(room, null, 2) }`); + + try { + await doAfterCreateRoom(room, users, subscriptions); + } catch (err) { + deleteRoom(room._id); + + logger.client.error(() => `afterCreateRoom => room=${ JSON.stringify(room, null, 2) } => Could not create federated room: ${ err }`); + } + + return room; +} + +export const definition = { + hook: 'afterCreateRoom', + callback: (roomOwner, room) => Promise.await(afterCreateRoom(roomOwner, room)), + id: 'federation-after-create-room', +}; diff --git a/app/federation/server/hooks/afterDeleteMessage.js b/app/federation/server/hooks/afterDeleteMessage.js new file mode 100644 index 00000000000..714de63a6e4 --- /dev/null +++ b/app/federation/server/hooks/afterDeleteMessage.js @@ -0,0 +1,28 @@ +import { FederationRoomEvents, Rooms } from '../../../models/server'; +import { logger } from '../lib/logger'; +import { hasExternalDomain } from '../functions/helpers'; +import { getFederationDomain } from '../lib/getFederationDomain'; +import { dispatchEvent } from '../handler'; + +async function afterDeleteMessage(message) { + const room = Rooms.findOneById(message.rid, { fields: { federation: 1 } }); + + // If there are not federated users on this room, ignore it + if (!hasExternalDomain(room)) { return message; } + + logger.client.debug(() => `afterDeleteMessage => message=${ JSON.stringify(message, null, 2) } room=${ JSON.stringify(room, null, 2) }`); + + // 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: (message) => Promise.await(afterDeleteMessage(message)), + id: 'federation-after-delete-message', +}; diff --git a/app/federation/server/hooks/afterMuteUser.js b/app/federation/server/hooks/afterMuteUser.js new file mode 100644 index 00000000000..4dba95ee4df --- /dev/null +++ b/app/federation/server/hooks/afterMuteUser.js @@ -0,0 +1,29 @@ +import { FederationRoomEvents } from '../../../models/server'; +import { logger } from '../lib/logger'; +import { normalizers } from '../normalizers'; +import { hasExternalDomain } from '../functions/helpers'; +import { getFederationDomain } from '../lib/getFederationDomain'; +import { dispatchEvent } from '../handler'; + +async function afterMuteUser(involvedUsers, room) { + // If there are not federated users on this room, ignore it + if (!hasExternalDomain(room)) { return involvedUsers; } + + logger.client.debug(() => `afterMuteUser => involvedUsers=${ JSON.stringify(involvedUsers, null, 2) } room=${ JSON.stringify(room, null, 2) }`); + + const { mutedUser } = involvedUsers; + + // Create the mute user event + const event = await FederationRoomEvents.createMuteUserEvent(getFederationDomain(), room._id, normalizers.normalizeUser(mutedUser)); + + // Dispatch event (async) + dispatchEvent(room.federation.domains, event); + + return involvedUsers; +} + +export const definition = { + hook: 'afterMuteUser', + callback: (involvedUsers, room) => Promise.await(afterMuteUser(involvedUsers, room)), + id: 'federation-after-mute-user', +}; diff --git a/app/federation/server/hooks/afterRemoveFromRoom.js b/app/federation/server/hooks/afterRemoveFromRoom.js new file mode 100644 index 00000000000..36ec2cebe17 --- /dev/null +++ b/app/federation/server/hooks/afterRemoveFromRoom.js @@ -0,0 +1,55 @@ +import { FederationRoomEvents } from '../../../models/server'; +import { getFederatedRoomData, hasExternalDomain, isLocalUser } from '../functions/helpers'; +import { logger } from '../lib/logger'; +import { normalizers } from '../normalizers'; +import { getFederationDomain } from '../lib/getFederationDomain'; +import { dispatchEvent } from '../handler'; + +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; + } + + logger.client.debug(() => `afterRemoveFromRoom => involvedUsers=${ JSON.stringify(involvedUsers, null, 2) } room=${ JSON.stringify(room, null, 2) }`); + + const { users } = getFederatedRoomData(room); + + try { + // Get the domains after removal + const domainsAfterRemoval = users.map((u) => u.federation.origin); + + // + // Normalize the room's federation status + // + const usersBeforeRemoval = users; + usersBeforeRemoval.push(removedUser); + + // Get the users domains + const domainsBeforeRemoval = usersBeforeRemoval.map((u) => u.federation.origin); + + // + // Create the user remove event + // + const normalizedSourceUser = normalizers.normalizeUser(removedUser); + + const removeUserEvent = await FederationRoomEvents.createRemoveUserEvent(localDomain, room._id, normalizedSourceUser, domainsAfterRemoval); + + // Dispatch the events + dispatchEvent(domainsBeforeRemoval, removeUserEvent); + } catch (err) { + logger.client.error(() => `afterRemoveFromRoom => involvedUsers=${ JSON.stringify(involvedUsers, null, 2) } => Could not add user: ${ err }`); + } + + return involvedUsers; +} + +export const definition = { + hook: 'afterRemoveFromRoom', + callback: (roomOwner, room) => Promise.await(afterRemoveFromRoom(roomOwner, room)), + id: 'federation-after-remove-from-room', +}; diff --git a/app/federation/server/hooks/afterSaveMessage.js b/app/federation/server/hooks/afterSaveMessage.js new file mode 100644 index 00000000000..5f72868a232 --- /dev/null +++ b/app/federation/server/hooks/afterSaveMessage.js @@ -0,0 +1,35 @@ +import { logger } from '../lib/logger'; +import { FederationRoomEvents } from '../../../models/server'; +import { normalizers } from '../normalizers'; +import { hasExternalDomain } from '../functions/helpers'; +import { getFederationDomain } from '../lib/getFederationDomain'; +import { dispatchEvent } from '../handler'; + +async function afterSaveMessage(message, room) { + // If there are not federated users on this room, ignore it + if (!hasExternalDomain(room)) { return message; } + + logger.client.debug(() => `afterSaveMessage => message=${ JSON.stringify(message, null, 2) } room=${ JSON.stringify(room, null, 2) }`); + + 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: (message, room) => Promise.await(afterSaveMessage(message, room)), + id: 'federation-after-save-message', +}; diff --git a/app/federation/server/hooks/afterSetReaction.js b/app/federation/server/hooks/afterSetReaction.js new file mode 100644 index 00000000000..fec108dd91d --- /dev/null +++ b/app/federation/server/hooks/afterSetReaction.js @@ -0,0 +1,30 @@ +import _ from 'underscore'; + +import { FederationRoomEvents, Rooms } from '../../../models/server'; +import { logger } from '../lib/logger'; +import { hasExternalDomain } from '../functions/helpers'; +import { getFederationDomain } from '../lib/getFederationDomain'; +import { dispatchEvent } from '../handler'; + +async function afterSetReaction(message, { user, reaction }) { + const room = Rooms.findOneById(message.rid, { fields: { federation: 1 } }); + + // If there are not federated users on this room, ignore it + if (!hasExternalDomain(room)) { return message; } + + logger.client.debug(() => `afterSetReaction => message=${ JSON.stringify(_.pick(message, '_id', 'msg'), null, 2) } room=${ JSON.stringify(_.pick(room, '_id'), null, 2) } user=${ JSON.stringify(_.pick(user, 'username'), null, 2) } reaction=${ 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: (message, extras) => Promise.await(afterSetReaction(message, extras)), + id: 'federation-after-set-reaction', +}; diff --git a/app/federation/server/hooks/afterUnmuteUser.js b/app/federation/server/hooks/afterUnmuteUser.js new file mode 100644 index 00000000000..3578e08c97d --- /dev/null +++ b/app/federation/server/hooks/afterUnmuteUser.js @@ -0,0 +1,29 @@ +import { FederationRoomEvents } from '../../../models/server'; +import { logger } from '../lib/logger'; +import { normalizers } from '../normalizers'; +import { hasExternalDomain } from '../functions/helpers'; +import { getFederationDomain } from '../lib/getFederationDomain'; +import { dispatchEvent } from '../handler'; + +async function afterUnmuteUser(involvedUsers, room) { + // If there are not federated users on this room, ignore it + if (!hasExternalDomain(room)) { return involvedUsers; } + + logger.client.debug(() => `afterUnmuteUser => involvedUsers=${ JSON.stringify(involvedUsers, null, 2) } room=${ JSON.stringify(room, null, 2) }`); + + const { unmutedUser } = involvedUsers; + + // Create the mute user event + const event = await FederationRoomEvents.createUnmuteUserEvent(getFederationDomain(), room._id, normalizers.normalizeUser(unmutedUser)); + + // Dispatch event (async) + dispatchEvent(room.federation.domains, event); + + return involvedUsers; +} + +export const definition = { + hook: 'afterUnmuteUser', + callback: (involvedUsers, room) => Promise.await(afterUnmuteUser(involvedUsers, room)), + id: 'federation-after-unmute-user', +}; diff --git a/app/federation/server/hooks/afterUnsetReaction.js b/app/federation/server/hooks/afterUnsetReaction.js new file mode 100644 index 00000000000..1ee72ff1f35 --- /dev/null +++ b/app/federation/server/hooks/afterUnsetReaction.js @@ -0,0 +1,29 @@ +import _ from 'underscore'; + +import { FederationRoomEvents, Rooms } from '../../../models/server'; +import { logger } from '../lib/logger'; +import { hasExternalDomain } from '../functions/helpers'; +import { getFederationDomain } from '../lib/getFederationDomain'; + +async function afterUnsetReaction(message, { user, reaction }) { + const room = Rooms.findOneById(message.rid, { fields: { federation: 1 } }); + + // If there are not federated users on this room, ignore it + if (!hasExternalDomain(room)) { return message; } + + logger.client.debug(() => `afterUnsetReaction => message=${ JSON.stringify(_.pick(message, '_id', 'msg'), null, 2) } room=${ JSON.stringify(_.pick(room, '_id'), null, 2) } user=${ JSON.stringify(_.pick(user, 'username'), null, 2) } reaction=${ 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: (message, extras) => Promise.await(afterUnsetReaction(message, extras)), + id: 'federation-after-unset-reaction', +}; diff --git a/app/federation/server/hooks/beforeDeleteRoom.js b/app/federation/server/hooks/beforeDeleteRoom.js new file mode 100644 index 00000000000..44005d9059d --- /dev/null +++ b/app/federation/server/hooks/beforeDeleteRoom.js @@ -0,0 +1,36 @@ +import { logger } from '../lib/logger'; +import { FederationRoomEvents, Rooms } from '../../../models/server'; +import { hasExternalDomain } from '../functions/helpers'; +import { getFederationDomain } from '../lib/getFederationDomain'; + +async function beforeDeleteRoom(roomId) { + const room = Rooms.findOneById(roomId, { fields: { 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; } + + logger.client.debug(() => `beforeDeleteRoom => room=${ JSON.stringify(room, null, 2) }`); + + try { + // Create the message event + const event = await FederationRoomEvents.createDeleteRoomEvent(getFederationDomain(), room._id); + + // Dispatch event (async) + dispatchEvent(room.federation.domains, event); + } catch (err) { + logger.client.error(() => `beforeDeleteRoom => room=${ JSON.stringify(room, null, 2) } => Could not remove room: ${ err }`); + + throw err; + } + + return roomId; +} + +export const definition = { + hook: 'beforeDeleteRoom', + callback: (roomId) => Promise.await(beforeDeleteRoom(roomId)), + id: 'federation-before-delete-room', +}; diff --git a/app/federation/server/index.js b/app/federation/server/index.js index f0c6844e86a..0fddfc2fe2e 100644 --- a/app/federation/server/index.js +++ b/app/federation/server/index.js @@ -1,149 +1,3 @@ -import { Meteor } from 'meteor/meteor'; -import { _ } from 'meteor/underscore'; - -import './federation-settings'; -import { logger } from './logger'; -import { PeerClient } from './PeerClient'; -import { PeerDNS } from './PeerDNS'; -import { PeerHTTP } from './PeerHTTP'; -import { PeerPinger } from './PeerPinger'; -import { PeerServer } from './PeerServer'; -import * as SettingsUpdater from './settingsUpdater'; -import './methods/dashboard'; -import { addUser } from './methods/addUser'; -import { searchUsers } from './methods/searchUsers'; -import { ping } from './methods/ping'; -import { FederationKeys } from '../../models'; -import { settings } from '../../settings'; -import { getConfig } from './config'; - -const peerClient = new PeerClient(); -const peerDNS = new PeerDNS(); -const peerHTTP = new PeerHTTP(); -const peerPinger = new PeerPinger(); -const peerServer = new PeerServer(); - -export const Federation = { - enabled: false, - privateKey: null, - publicKey: null, - usingHub: null, - uniqueId: null, - localIdentifier: null, - - peerClient, - peerDNS, - peerHTTP, - peerPinger, - peerServer, -}; - -// Add Federation methods -Federation.methods = { - addUser, - searchUsers, - ping, -}; - -// Generate keys - -// Create unique id if needed -if (!FederationKeys.getUniqueId()) { - FederationKeys.generateUniqueId(); -} - -// Create key pair if needed -if (!FederationKeys.getPublicKey()) { - FederationKeys.generateKeys(); -} - -// Initializations - -// Start the client, setting up all the callbacks -peerClient.start(); - -// Start the server, setting up all the endpoints -peerServer.start(); - -// Start the pinger, to check the status of all peers -peerPinger.start(); - -const updateSettings = _.debounce(Meteor.bindEnvironment(function() { - const _enabled = settings.get('FEDERATION_Enabled'); - - if (!_enabled) { return; } - - const config = getConfig(); - - // If the settings are correctly set, let's update the configuration - - // Get the key pair - Federation.privateKey = FederationKeys.getPrivateKey(); - Federation.publicKey = FederationKeys.getPublicKey(); - - // Set important information - Federation.enabled = true; - Federation.usingHub = config.hub.active; - Federation.uniqueId = config.peer.uniqueId; - Federation.localIdentifier = config.peer.domain; - - // Set DNS - peerDNS.setConfig(config); - - // Set HTTP - peerHTTP.setConfig(config); - - // Set Client - peerClient.setConfig(config); - peerClient.enable(); - - // Set server - peerServer.setConfig(config); - peerServer.enable(); - - // Register the client - if (peerClient.register()) { - SettingsUpdater.updateStatus('Running'); - } else { - SettingsUpdater.updateNextStatusTo('Disabled, could not register with Hub'); - SettingsUpdater.updateEnabled(false); - } -}), 150); - -function enableOrDisable() { - const _enabled = settings.get('FEDERATION_Enabled'); - - // If it was enabled, and was disabled now, - // make sure we disable everything: callbacks and endpoints - if (Federation.enabled && !_enabled) { - peerClient.disable(); - peerServer.disable(); - - // Disable federation - Federation.enabled = false; - - SettingsUpdater.updateStatus('Disabled'); - - logger.setup.info('Shutting down...'); - - return; - } - - // If not enabled, skip - if (!_enabled) { - SettingsUpdater.updateStatus('Disabled'); - return; - } - - logger.setup.info('Booting...'); - - SettingsUpdater.updateStatus('Booting...'); - - updateSettings(); -} - -// Add settings listeners -settings.get('FEDERATION_Enabled', enableOrDisable); -settings.get('FEDERATION_Domain', updateSettings); -settings.get('FEDERATION_Discovery_Method', updateSettings); -settings.get('FEDERATION_Hub_URL', updateSettings); +import './methods'; +import './endpoints'; +import './startup'; diff --git a/app/federation/server/lib/callbacks.js b/app/federation/server/lib/callbacks.js new file mode 100644 index 00000000000..c2712ad80d3 --- /dev/null +++ b/app/federation/server/lib/callbacks.js @@ -0,0 +1,19 @@ +import { callbacks } from '../../../callbacks/server'; + +const callbackDefinitions = []; + +export function registerCallback(callbackDefition) { + callbackDefinitions.push(callbackDefition); +} + +export function enableCallbacks() { + for (const definition of callbackDefinitions) { + callbacks.add(definition.hook, definition.callback, callbacks.priority.LOW, definition.id); + } +} + +export function disableCallbacks() { + for (const definition of callbackDefinitions) { + callbacks.remove(definition.hook, definition.id); + } +} diff --git a/app/federation/server/lib/crypt.js b/app/federation/server/lib/crypt.js new file mode 100644 index 00000000000..ce92cfd8405 --- /dev/null +++ b/app/federation/server/lib/crypt.js @@ -0,0 +1,67 @@ +import { FederationKeys } from '../../../models/server'; +import { getFederationDomain } from './getFederationDomain'; +import { search } from './dns'; +import { logger } from './logger'; + +export function decrypt(data, peerKey) { + // + // Decrypt the payload + const payloadBuffer = Buffer.from(data); + + // Decrypt with the peer's public key + try { + data = FederationKeys.loadKey(peerKey, 'public').decryptPublic(payloadBuffer); + + // Decrypt with the local private key + data = FederationKeys.getPrivateKey().decrypt(data); + } catch (err) { + logger.crypt.error(err); + + throw new Error('Could not decrypt'); + } + + return JSON.parse(data.toString()); +} + +export 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 } = search(remotePeerDomain); + + if (!peerKey) { + throw new Error("Could not find the peer's public key to decrypt"); + } + + return decrypt(bodyParams, peerKey); +} + +export function encrypt(data, peerKey) { + if (!data) { + return data; + } + + try { + // Encrypt with the peer's public key + data = FederationKeys.loadKey(peerKey, 'public').encrypt(data); + + // Encrypt with the local private key + return FederationKeys.getPrivateKey().encryptPrivate(data); + } catch (err) { + logger.crypt.error(err); + + throw new Error('Could not encrypt'); + } +} diff --git a/app/federation/server/lib/dns.js b/app/federation/server/lib/dns.js new file mode 100644 index 00000000000..b6a58ba21ff --- /dev/null +++ b/app/federation/server/lib/dns.js @@ -0,0 +1,104 @@ +import dnsResolver from 'dns'; + +import { Meteor } from 'meteor/meteor'; + +import * as federationErrors from '../functions/errors'; +import { logger } from './logger'; +import { isFederationEnabled } from './isFederationEnabled'; +import { federationRequest } from './http'; + +const dnsResolveSRV = Meteor.wrapAsync(dnsResolver.resolveSrv); +const dnsResolveTXT = Meteor.wrapAsync(dnsResolver.resolveTxt); + +const hubUrl = process.env.NODE_ENV === 'development' ? 'http://localhost:8080' : 'https://hub.rocket.chat'; + +export 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 + federationRequest('POST', `${ hubUrl }/api/v1/peers`, body); + + return true; + } catch (err) { + logger.dns.error(err); + + throw federationErrors.peerCouldNotBeRegisteredWithHub('dns.registerWithHub'); + } +} + +export function searchHub(peerDomain) { + try { + // If there is no DNS entry for that, get from the Hub + const { data: { peer } } = federationRequest('GET', `${ hubUrl }/api/v1/peers?search=${ peerDomain }`); + + if (!peer) { + throw federationErrors.peerCouldNotBeRegisteredWithHub('dns.registerWithHub'); + } + + const { url, public_key: publicKey } = peer; + + return { + url, + peerDomain, + publicKey, + }; + } catch (err) { + logger.dns.error(err); + + throw federationErrors.peerNotFoundUsingDNS('dns.searchHub'); + } +} + +export function search(peerDomain) { + if (!isFederationEnabled()) { + throw federationErrors.disabled('dns.search'); + } + + logger.dns.debug(`search: ${ peerDomain }`); + + let srvEntries = []; + let protocol = ''; + + // Search by HTTPS first + try { + srvEntries = dnsResolveSRV(`_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 { + srvEntries = dnsResolveSRV(`_rocketchat._http.${ peerDomain }`); + protocol = 'http'; + } catch (err) { + // Ignore errors when looking for DNS entries + } + } + + const [srvEntry] = srvEntries; + + // If there is no entry, throw error + if (!srvEntry) { + return searchHub(peerDomain); + } + + // Get the public key from the TXT record + const publicKeyTxtRecords = dnsResolveTXT(`rocketchat-public-key.${ peerDomain }`); + + // Join the TXT record, that might be split + const publicKey = publicKeyTxtRecords[0].join(''); + + // If there is no entry, throw error + if (!publicKey) { + return searchHub(peerDomain); + } + + return { + url: `${ protocol }://${ srvEntry.name }:${ srvEntry.port }`, + peerDomain, + publicKey, + }; +} diff --git a/app/federation/server/lib/getFederationDiscoveryMethod.js b/app/federation/server/lib/getFederationDiscoveryMethod.js new file mode 100644 index 00000000000..2da490942fd --- /dev/null +++ b/app/federation/server/lib/getFederationDiscoveryMethod.js @@ -0,0 +1,3 @@ +import { settings } from '../../../settings/server'; + +export const getFederationDiscoveryMethod = () => settings.get('FEDERATION_Discovery_Method'); diff --git a/app/federation/server/lib/getFederationDomain.js b/app/federation/server/lib/getFederationDomain.js new file mode 100644 index 00000000000..c5e67629db7 --- /dev/null +++ b/app/federation/server/lib/getFederationDomain.js @@ -0,0 +1,3 @@ +import { settings } from '../../../settings/server'; + +export const getFederationDomain = () => settings.get('FEDERATION_Domain').replace('@', ''); diff --git a/app/federation/server/lib/http.js b/app/federation/server/lib/http.js new file mode 100644 index 00000000000..191e7a473ea --- /dev/null +++ b/app/federation/server/lib/http.js @@ -0,0 +1,52 @@ +import { HTTP as MeteorHTTP } from 'meteor/http'; +import { EJSON } from 'meteor/ejson'; + +import { logger } from './logger'; +import { getFederationDomain } from './getFederationDomain'; +import { search } from './dns'; +import { encrypt } from './crypt'; + +export function federationRequest(method, url, body, headers, peerKey = null) { + let data = null; + + if ((method === 'POST' || method === 'PUT') && body) { + data = EJSON.toJSONValue(body); + + if (peerKey) { + data = encrypt(data, peerKey); + } + } + + logger.http.debug(`[${ method }] ${ url }`); + + return MeteorHTTP.call(method, url, { data, timeout: 2000, headers: { ...headers, 'x-federation-domain': getFederationDomain() } }); +} + +export 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 { + result = federationRequest(method, `${ baseUrl }${ uri }`, body, options.headers || {}, peerKey); + } catch (err) { + logger.http.error(`${ ignoreErrors ? '[IGNORED] ' : '' }Error ${ err }`); + + if (!ignoreErrors) { + throw err; + } else { + return { success: false }; + } + } + + return { success: true, data: result.data }; +} diff --git a/app/federation/server/lib/isFederationEnabled.js b/app/federation/server/lib/isFederationEnabled.js new file mode 100644 index 00000000000..9e46d3004ac --- /dev/null +++ b/app/federation/server/lib/isFederationEnabled.js @@ -0,0 +1,3 @@ +import { settings } from '../../../settings/server'; + +export const isFederationEnabled = () => settings.get('FEDERATION_Enabled'); diff --git a/app/federation/server/lib/logger.js b/app/federation/server/lib/logger.js new file mode 100644 index 00000000000..f3e791a14bf --- /dev/null +++ b/app/federation/server/lib/logger.js @@ -0,0 +1,12 @@ +import { Logger } from '../../../logger/server'; + +export const logger = new Logger('Federation', { + sections: { + client: 'client', + crypt: 'crypt', + dns: 'dns', + http: 'http', + server: 'server', + setup: 'Setup', + }, +}); diff --git a/app/federation/server/logger.js b/app/federation/server/logger.js deleted file mode 100644 index 77ffd9c4ab8..00000000000 --- a/app/federation/server/logger.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Logger } from '../../logger'; - -export const logger = new Logger('Federation', { - sections: { - resource: 'Resource', - setup: 'Setup', - peerClient: 'Peer Client', - peerServer: 'Peer Server', - dns: 'DNS', - http: 'HTTP', - pinger: 'Pinger', - }, -}); diff --git a/app/federation/server/methods/addUser.js b/app/federation/server/methods/addUser.js deleted file mode 100644 index 9a0d7a3ceaa..00000000000 --- a/app/federation/server/methods/addUser.js +++ /dev/null @@ -1,50 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Users, FederationPeers } from '../../../models'; - -import { Federation } from '..'; - -import { logger } from '../logger'; - -export function addUser(identifier) { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'Federation.addUser' }); - } - - if (!Federation.peerServer.enabled) { - throw new Meteor.Error('error-federation-disabled', 'Federation disabled', { method: 'Federation.addUser' }); - } - - // Make sure the federated user still exists, and get the unique one, by email address - const [federatedUser] = Federation.peerClient.findUsers(identifier, { usernameOnly: true }); - - if (!federatedUser) { - throw new Meteor.Error('federation-invalid-user', 'There is no user to add.'); - } - - let user = null; - - const localUser = federatedUser.getLocalUser(); - - localUser.name += `@${ federatedUser.user.federation.peer }`; - - // Delete the _id - delete localUser._id; - - try { - // Create the local user - user = Users.create(localUser); - - // Refresh the peers list - FederationPeers.refreshPeers(); - } catch (err) { - // If the user already exists, return the existing user - if (err.code === 11000) { - user = Users.findOne({ 'federation._id': localUser.federation._id }); - } - - logger.error(err); - } - - return user; -} diff --git a/app/federation/server/methods/dashboard.js b/app/federation/server/methods/dashboard.js index d272991cd64..49a374141b4 100644 --- a/app/federation/server/methods/dashboard.js +++ b/app/federation/server/methods/dashboard.js @@ -1,76 +1,8 @@ import { Meteor } from 'meteor/meteor'; -import moment from 'moment'; -// We do not import the whole Federation object here because statistics cron -// job use this file, and some of the features are not available on the cron -import { FederationEvents, FederationPeers, Users } from '../../../models/server'; - -export function getStatistics() { - const numberOfEvents = FederationEvents.findByType('png').count(); - const numberOfFederatedUsers = Users.findRemote().count(); - const numberOfActivePeers = FederationPeers.findActiveRemote().count(); - const numberOfInactivePeers = FederationPeers.findNotActiveRemote().count(); - - return { numberOfEvents, numberOfFederatedUsers, numberOfActivePeers, numberOfInactivePeers }; -} - -export function federationGetOverviewData() { - if (!Meteor.userId()) { - throw new Meteor.Error('not-authorized'); - } - - const { numberOfEvents, numberOfFederatedUsers, numberOfActivePeers, numberOfInactivePeers } = getStatistics(); - - return { - data: [{ - title: 'Number_of_events', - value: numberOfEvents, - }, { - title: 'Number_of_federated_users', - value: numberOfFederatedUsers, - }, { - title: 'Number_of_active_peers', - value: numberOfActivePeers, - }, { - title: 'Number_of_inactive_peers', - value: numberOfInactivePeers, - }], - }; -} - -export function federationGetPeerStatuses() { - if (!Meteor.userId()) { - throw new Meteor.Error('not-authorized'); - } - - const peers = FederationPeers.findRemote().fetch(); - - const peerStatuses = []; - - const stabilityLimit = moment().subtract(5, 'days'); - - for (const { peer, active, last_seen_at: lastSeenAt, last_failure_at: lastFailureAt } of peers) { - let status = 'failing'; - - if (active && lastFailureAt && moment(lastFailureAt).isAfter(stabilityLimit)) { - status = 'unstable'; - } else if (active) { - status = 'stable'; - } - - peerStatuses.push({ - peer, - status, - statusAt: active ? lastSeenAt : lastFailureAt, - }); - } - - return { - data: peerStatuses, - }; -} +import { federationGetServers, federationGetOverviewData } from '../functions/dashboard'; Meteor.methods({ 'federation:getOverviewData': federationGetOverviewData, - 'federation:getPeerStatuses': federationGetPeerStatuses, + 'federation:getServers': federationGetServers, }); diff --git a/app/federation/server/methods/index.js b/app/federation/server/methods/index.js new file mode 100644 index 00000000000..26d3e1f619f --- /dev/null +++ b/app/federation/server/methods/index.js @@ -0,0 +1,3 @@ +import './dashboard'; +import './loadContextEvents'; +import './testSetup'; diff --git a/app/federation/server/methods/loadContextEvents.js b/app/federation/server/methods/loadContextEvents.js new file mode 100644 index 00000000000..c68700d3ab4 --- /dev/null +++ b/app/federation/server/methods/loadContextEvents.js @@ -0,0 +1,18 @@ +import { Meteor } from 'meteor/meteor'; + +import { hasRole } from '../../../authorization/server'; +import { FederationRoomEvents } from '../../../models/server'; + +Meteor.methods({ + 'federation:loadContextEvents': (latestEventTimestamp) => { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'loadContextEvents' }); + } + + if (!hasRole(Meteor.userId(), 'admin')) { + throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'loadContextEvents' }); + } + + return FederationRoomEvents.find({ timestamp: { $gt: new Date(latestEventTimestamp) } }, { sort: { timestamp: 1 } }).fetch(); + }, +}); diff --git a/app/federation/server/methods/ping.js b/app/federation/server/methods/ping.js deleted file mode 100644 index 30cace3ad96..00000000000 --- a/app/federation/server/methods/ping.js +++ /dev/null @@ -1,54 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { FederationEvents } from '../../../models'; -import { settings } from '../../../settings'; -import { delay } from '../PeerHTTP/utils'; - -export function ping(peers, timeToWait = 5000) { - // Create the ping events - const pingEvents = FederationEvents.ping(peers); - - // Make sure timeToWait is at least one second - timeToWait = timeToWait < 1000 ? 1000 : timeToWait; - - const results = {}; - - while (timeToWait > 0) { - timeToWait -= 500; - delay(500); - - for (const { _id: pingEventId } of pingEvents) { - // Get the ping event - const pingEvent = FederationEvents.findOne({ _id: pingEventId }); - - if (!pingEvent.fulfilled && !pingEvent.error) { continue; } - - // If there is an error or the event is fulfilled, it means it is already handled. - // Given that, fulfilled will be true if everything went well, or false if there was an error; - results[pingEvent.peer] = pingEvent.fulfilled; - } - - // If we already have all the results, break - if (Object.keys(results).length === peers.length) { - break; - } - } - - return results; -} - -Meteor.methods({ - FEDERATION_Test_Setup() { - const localPeer = settings.get('FEDERATION_Domain'); - - const results = ping([localPeer]); - - if (!results[localPeer]) { - throw new Meteor.Error('FEDERATION_Test_Setup_Error'); - } - - return { - message: 'FEDERATION_Test_Setup_Success', - }; - }, -}); diff --git a/app/federation/server/methods/searchUsers.js b/app/federation/server/methods/searchUsers.js deleted file mode 100644 index b27d411213b..00000000000 --- a/app/federation/server/methods/searchUsers.js +++ /dev/null @@ -1,21 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Federation } from '..'; - -export function searchUsers(identifier) { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'federationSearchUsers' }); - } - - if (!Federation.peerClient.enabled) { - throw new Meteor.Error('error-federation-disabled', 'Federation disabled', { method: 'federationSearchUsers' }); - } - - const federatedUsers = Federation.peerClient.findUsers(identifier); - - if (!federatedUsers.length) { - throw new Meteor.Error('federation-user-not-found', `Could not find federated users using "${ identifier }"`); - } - - return federatedUsers; -} diff --git a/app/federation/server/methods/testSetup.js b/app/federation/server/methods/testSetup.js new file mode 100644 index 00000000000..0e242fac376 --- /dev/null +++ b/app/federation/server/methods/testSetup.js @@ -0,0 +1,21 @@ +import { Meteor } from 'meteor/meteor'; + +import { eventTypes } from '../../../models/server/models/FederationEvents'; +import { getFederationDomain } from '../lib/getFederationDomain'; +import { dispatchEvent } from '../handler'; + +Meteor.methods({ + FEDERATION_Test_Setup() { + try { + dispatchEvent([getFederationDomain()], { + type: eventTypes.PING, + }); + + return { + message: 'FEDERATION_Test_Setup_Success', + }; + } catch (err) { + throw new Meteor.Error('FEDERATION_Test_Setup_Error'); + } + }, +}); diff --git a/app/federation/server/normalizers/index.js b/app/federation/server/normalizers/index.js new file mode 100644 index 00000000000..ae1e2183626 --- /dev/null +++ b/app/federation/server/normalizers/index.js @@ -0,0 +1,11 @@ +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/app/federation/server/normalizers/message.js b/app/federation/server/normalizers/message.js new file mode 100644 index 00000000000..004c7fe6d57 --- /dev/null +++ b/app/federation/server/normalizers/message.js @@ -0,0 +1,96 @@ +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/app/federation/server/normalizers/room.js b/app/federation/server/normalizers/room.js new file mode 100644 index 00000000000..4aceb9dbdb9 --- /dev/null +++ b/app/federation/server/normalizers/room.js @@ -0,0 +1,93 @@ +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)); + } + } + + // 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/app/federation/server/normalizers/subscription.js b/app/federation/server/normalizers/subscription.js new file mode 100644 index 00000000000..814ac79696f --- /dev/null +++ b/app/federation/server/normalizers/subscription.js @@ -0,0 +1,42 @@ +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/app/federation/server/normalizers/user.js b/app/federation/server/normalizers/user.js new file mode 100644 index 00000000000..8dbc50a1682 --- /dev/null +++ b/app/federation/server/normalizers/user.js @@ -0,0 +1,61 @@ +import _ from 'underscore'; + +import { Users } from '../../../models/server'; +import { getNameAndDomain, isFullyQualified } from '../functions/helpers'; +import { getFederationDomain } from '../lib/getFederationDomain'; + +const denormalizeUser = (originalResource) => { + const resource = { ...originalResource }; + + 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 = (originalResource) => { + // Get only what we need, non-sensitive data + const resource = _.pick(originalResource, '_id', 'username', 'type', 'emails', 'name', 'federation', 'isRemote', 'createdAt', '_updatedAt'); + + const email = resource.emails[0].address; + + resource.emails = [{ + address: `${ resource._id }@${ getFederationDomain() }`, + }]; + + 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 + Users.update({ _id: resource._id }, { $set: { isRemote: resource.isRemote, federation: resource.federation } }); + + return resource; +}; + +const normalizeAllUsers = (resources) => resources.map(normalizeUser); + +export default { + denormalizeUser, + denormalizeAllUsers, + normalizeUser, + normalizeAllUsers, +}; diff --git a/app/federation/server/settingsUpdater.js b/app/federation/server/settingsUpdater.js deleted file mode 100644 index 344d6545e95..00000000000 --- a/app/federation/server/settingsUpdater.js +++ /dev/null @@ -1,17 +0,0 @@ -import { Settings } from '../../models'; - -let nextStatus; - -export function updateStatus(status) { - Settings.updateValueById('FEDERATION_Status', nextStatus || status); - - nextStatus = null; -} - -export function updateNextStatusTo(status) { - nextStatus = status; -} - -export function updateEnabled(enabled) { - Settings.updateValueById('FEDERATION_Enabled', enabled); -} diff --git a/app/federation/server/startup/generateKeys.js b/app/federation/server/startup/generateKeys.js new file mode 100644 index 00000000000..012cdd0b48f --- /dev/null +++ b/app/federation/server/startup/generateKeys.js @@ -0,0 +1,6 @@ +import { FederationKeys } from '../../../models/server'; + +// Create key pair if needed +if (!FederationKeys.getPublicKey()) { + FederationKeys.generateKeys(); +} diff --git a/app/federation/server/startup/index.js b/app/federation/server/startup/index.js new file mode 100644 index 00000000000..17714a5c69c --- /dev/null +++ b/app/federation/server/startup/index.js @@ -0,0 +1,3 @@ +import './generateKeys'; +import './settings'; +import './registerCallbacks'; diff --git a/app/federation/server/startup/registerCallbacks.js b/app/federation/server/startup/registerCallbacks.js new file mode 100644 index 00000000000..677b4daddcc --- /dev/null +++ b/app/federation/server/startup/registerCallbacks.js @@ -0,0 +1,24 @@ +import { registerCallback } from '../lib/callbacks'; +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 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'; + +registerCallback(afterAddedToRoomDef); +registerCallback(afterCreateDirectRoomDef); +registerCallback(afterCreateRoomDef); +registerCallback(afterDeleteMessageDef); +registerCallback(afterMuteUserDef); +registerCallback(beforeDeleteRoomDef); +registerCallback(afterSaveMessageDef); +registerCallback(afterSetReactionDef); +registerCallback(afterUnmuteUserDef); +registerCallback(afterUnsetReactionDef); +registerCallback(afterRemoveFromRoomDef); diff --git a/app/federation/server/startup/settings.js b/app/federation/server/startup/settings.js new file mode 100644 index 00000000000..3bcb454523a --- /dev/null +++ b/app/federation/server/startup/settings.js @@ -0,0 +1,105 @@ +import { debounce } from 'underscore'; +import { Meteor } from 'meteor/meteor'; + +import { settings } from '../../../settings/server'; +import { updateStatus, updateEnabled } from '../functions/helpers'; +import { getFederationDomain } from '../lib/getFederationDomain'; +import { getFederationDiscoveryMethod } from '../lib/getFederationDiscoveryMethod'; +import { registerWithHub } from '../lib/dns'; +import { enableCallbacks, disableCallbacks } from '../lib/callbacks'; +import { logger } from '../lib/logger'; +import { FederationKeys } from '../../../models/server'; + +Meteor.startup(function() { + const federationPublicKey = FederationKeys.getPublicKeyString(); + + settings.addGroup('Federation', function() { + this.add('FEDERATION_Enabled', false, { + type: 'boolean', + i18nLabel: 'Enabled', + i18nDescription: 'FEDERATION_Enabled', + alert: 'FEDERATION_Enabled_Alert', + public: true, + }); + + this.add('FEDERATION_Status', 'Disabled', { + readonly: true, + type: 'string', + i18nLabel: 'FEDERATION_Status', + }); + + this.add('FEDERATION_Domain', '', { + type: 'string', + i18nLabel: 'FEDERATION_Domain', + i18nDescription: 'FEDERATION_Domain_Description', + alert: 'FEDERATION_Domain_Alert', + disableReset: true, + }); + + this.add('FEDERATION_Public_Key', federationPublicKey, { + readonly: true, + type: 'string', + multiline: true, + i18nLabel: 'FEDERATION_Public_Key', + i18nDescription: 'FEDERATION_Public_Key_Description', + }); + + this.add('FEDERATION_Discovery_Method', 'dns', { + type: 'select', + values: [{ + key: 'dns', + i18nLabel: 'DNS', + }, { + key: 'hub', + i18nLabel: 'Hub', + }], + i18nLabel: 'FEDERATION_Discovery_Method', + i18nDescription: 'FEDERATION_Discovery_Method_Description', + public: true, + }); + + this.add('FEDERATION_Test_Setup', 'FEDERATION_Test_Setup', { + type: 'action', + actionText: 'FEDERATION_Test_Setup', + }); + }); +}); + +const updateSettings = debounce(Meteor.bindEnvironment(function() { + // Get the key pair + + if (getFederationDiscoveryMethod() === 'hub') { + // Register with hub + try { + registerWithHub(getFederationDomain(), settings.get('Site_Url'), FederationKeys.getPublicKeyString()); + } catch (err) { + // Disable federation + updateEnabled(false); + + updateStatus('Could not register with Hub'); + } + } else { + updateStatus('Enabled'); + } +}), 150); + +function enableOrDisable(key, value) { + logger.setup.info(`Federation is ${ value ? 'enabled' : 'disabled' }`); + + if (value) { + updateSettings(); + + enableCallbacks(); + } else { + updateStatus('Disabled'); + + disableCallbacks(); + } + + value && updateSettings(); +} + +// Add settings listeners +settings.get('FEDERATION_Enabled', enableOrDisable); +settings.get('FEDERATION_Domain', updateSettings); +settings.get('FEDERATION_Discovery_Method', updateSettings); diff --git a/app/lib/server/functions/createDirectRoom.js b/app/lib/server/functions/createDirectRoom.js new file mode 100644 index 00000000000..67d516adff7 --- /dev/null +++ b/app/lib/server/functions/createDirectRoom.js @@ -0,0 +1,47 @@ +import { Rooms, Subscriptions } from '../../../models/server'; + +export const createDirectRoom = function(source, target, extraData, options) { + const rid = [source._id, target._id].sort().join(''); + + Rooms.upsert({ _id: rid }, { + $setOnInsert: Object.assign({ + t: 'd', + usernames: [source.username, target.username], + msgs: 0, + ts: new Date(), + }, extraData), + }); + + Subscriptions.upsert({ rid, 'u._id': target._id }, { + $setOnInsert: Object.assign({ + name: source.username, + t: 'd', + open: true, + alert: true, + unread: 0, + u: { + _id: target._id, + username: target.username, + }, + }, options.subscriptionExtra), + }); + + Subscriptions.upsert({ rid, 'u._id': source._id }, { + $setOnInsert: Object.assign({ + name: target.username, + t: 'd', + open: true, + alert: true, + unread: 0, + u: { + _id: source._id, + username: source.username, + }, + }, options.subscriptionExtra), + }); + + return { + _id: rid, + t: 'd', + }; +}; diff --git a/app/lib/server/functions/createRoom.js b/app/lib/server/functions/createRoom.js index 31eb76285e1..116c6f5e92d 100644 --- a/app/lib/server/functions/createRoom.js +++ b/app/lib/server/functions/createRoom.js @@ -7,52 +7,7 @@ import { callbacks } from '../../../callbacks'; import { addUserRoles } from '../../../authorization'; import { getValidRoomName } from '../../../utils'; import { Apps } from '../../../apps/server'; - -function createDirectRoom(source, target, extraData, options) { - const rid = [source._id, target._id].sort().join(''); - - Rooms.upsert({ _id: rid }, { - $setOnInsert: Object.assign({ - t: 'd', - usernames: [source.username, target.username], - msgs: 0, - ts: new Date(), - }, extraData), - }); - - Subscriptions.upsert({ rid, 'u._id': target._id }, { - $setOnInsert: Object.assign({ - name: source.username, - t: 'd', - open: true, - alert: true, - unread: 0, - u: { - _id: target._id, - username: target.username, - }, - }, options.subscriptionExtra), - }); - - Subscriptions.upsert({ rid, 'u._id': source._id }, { - $setOnInsert: Object.assign({ - name: target.username, - t: 'd', - open: true, - alert: true, - unread: 0, - u: { - _id: source._id, - username: source.username, - }, - }, options.subscriptionExtra), - }); - - return { - _id: rid, - t: 'd', - }; -} +import { createDirectRoom } from './createDirectRoom'; export const createRoom = function(type, name, owner, members, readOnly, extraData = {}, options = {}) { if (type === 'd') { diff --git a/app/lib/server/functions/deleteRoom.js b/app/lib/server/functions/deleteRoom.js index 8d113906d8b..a00d11d0acf 100644 --- a/app/lib/server/functions/deleteRoom.js +++ b/app/lib/server/functions/deleteRoom.js @@ -4,6 +4,7 @@ import { callbacks } from '../../../callbacks'; export const deleteRoom = function(rid) { Messages.removeFilesByRoomId(rid); Messages.removeByRoomId(rid); + callbacks.run('beforeDeleteRoom', rid); Subscriptions.removeByRoomId(rid); callbacks.run('afterDeleteRoom', rid); return Rooms.removeById(rid); diff --git a/app/lib/server/functions/deleteUser.js b/app/lib/server/functions/deleteUser.js index e40b6c88f49..5f127edb1bf 100644 --- a/app/lib/server/functions/deleteUser.js +++ b/app/lib/server/functions/deleteUser.js @@ -2,17 +2,24 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { FileUpload } from '../../../file-upload'; -import { Users, Subscriptions, Messages, Rooms, Integrations, FederationPeers } from '../../../models'; +import { Users, Subscriptions, Messages, Rooms, Integrations, FederationServers } from '../../../models'; import { hasRole, getUsersInRole } from '../../../authorization'; import { settings } from '../../../settings'; import { Notifications } from '../../../notifications'; -import { getConfig } from '../../../federation/server/config'; export const deleteUser = function(userId) { const user = Users.findOneById(userId, { - fields: { username: 1, avatarOrigin: 1 }, + fields: { username: 1, avatarOrigin: 1, federation: 1 }, }); + if (user.federation) { + const existingSubscriptions = Subscriptions.find({ 'u._id': user._id }).count(); + + if (existingSubscriptions > 0) { + throw new Meteor.Error('FEDERATION_Error_user_is_federated_on_rooms'); + } + } + // Users without username can't do anything, so there is nothing to remove if (user.username != null) { const roomCache = []; @@ -99,7 +106,6 @@ export const deleteUser = function(userId) { Users.removeById(userId); // Remove user from users database - // Refresh the peers list - const { peer: { domain: localPeerDomain } } = getConfig(); - FederationPeers.refreshPeers(localPeerDomain); + // Refresh the servers list + FederationServers.refreshServers(); }; diff --git a/app/lib/server/functions/index.js b/app/lib/server/functions/index.js index 7a94e02f36f..f318ad2ccbb 100644 --- a/app/lib/server/functions/index.js +++ b/app/lib/server/functions/index.js @@ -6,6 +6,7 @@ export { checkEmailAvailability } from './checkEmailAvailability'; export { checkUsernameAvailability } from './checkUsernameAvailability'; export { cleanRoomHistory } from './cleanRoomHistory'; export { createRoom } from './createRoom'; +export { createDirectRoom } from './createDirectRoom'; export { deleteMessage } from './deleteMessage'; export { deleteRoom } from './deleteRoom'; export { deleteUser } from './deleteUser'; diff --git a/app/logger/server/server.js b/app/logger/server/server.js index 6d15ed22883..40c3c7dc823 100644 --- a/app/logger/server/server.js +++ b/app/logger/server/server.js @@ -289,6 +289,11 @@ class _Logger { return; } + // Deferred logging + if (typeof options.arguments[0] === 'function') { + options.arguments[0] = options.arguments[0](); + } + const prefix = this.getPrefix(options); if (options.box === true && _.isString(options.arguments[0])) { diff --git a/app/models/client/models/FederationPeers.js b/app/models/client/models/FederationPeers.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/models/server/index.js b/app/models/server/index.js index f788aed8eaa..81b288f34d5 100644 --- a/app/models/server/index.js +++ b/app/models/server/index.js @@ -38,9 +38,9 @@ export { AppsLogsModel } from './models/apps-logs-model'; export { AppsPersistenceModel } from './models/apps-persistence-model'; export { AppsModel } from './models/apps-model'; export { FederationDNSCache } from './models/FederationDNSCache'; -export { FederationEvents } from './models/FederationEvents'; +export { FederationRoomEvents } from './models/FederationRoomEvents'; export { FederationKeys } from './models/FederationKeys'; -export { FederationPeers } from './models/FederationPeers'; +export { FederationServers } from './models/FederationServers'; export { Base, diff --git a/app/models/server/models/FederationEvents.js b/app/models/server/models/FederationEvents.js index 55dbcb366ac..bde42d59eb1 100644 --- a/app/models/server/models/FederationEvents.js +++ b/app/models/server/models/FederationEvents.js @@ -1,271 +1,160 @@ -import { Meteor } from 'meteor/meteor'; +import { SHA256 } from 'meteor/sha'; import { Base } from './_Base'; -const normalizePeers = (basePeers, options) => { - const { peers: sentPeers, skipPeers } = options; - - let peers = sentPeers || basePeers || []; - - if (skipPeers) { - peers = peers.filter((p) => skipPeers.indexOf(p) === -1); - } - - return peers; +export const eventTypes = { + // Global + GENESIS: 'genesis', + PING: 'ping', + + // Room + ROOM_DELETE: 'room_delete', + ROOM_ADD_USER: 'room_add_user', + ROOM_REMOVE_USER: 'room_remove_user', + ROOM_MESSAGE: 'room_message', + ROOM_EDIT_MESSAGE: 'room_edit_message', + ROOM_DELETE_MESSAGE: 'room_delete_message', + ROOM_SET_MESSAGE_REACTION: 'room_set_message_reaction', + ROOM_UNSET_MESSAGE_REACTION: 'room_unset_message_reaction', + ROOM_MUTE_USER: 'room_mute_user', + ROOM_UNMUTE_USER: 'room_unmute_user', }; -// -// We should create a time to live index in this table to remove fulfilled events -// -class FederationEventsModel extends Base { - constructor() { - super('federation_events'); - - this.tryEnsureIndex({ t: 1 }); - this.tryEnsureIndex({ fulfilled: 1 }); - this.tryEnsureIndex({ ts: 1 }); - } - - // Sometimes events errored but the error is final - setEventAsErrored(e, error, fulfilled = false) { - this.update({ _id: e._id }, { - $set: { - fulfilled, - lastAttemptAt: new Date(), - error, - }, - }); - } - - setEventAsFullfilled(e) { - this.update({ _id: e._id }, { - $set: { fulfilled: true }, - $unset: { error: 1 }, - }); - } - - createEvent(type, payload, peer, options) { - const record = { - t: type, - ts: new Date(), - fulfilled: false, - payload, - peer, - options, - }; - - record._id = this.insert(record); - - Meteor.defer(() => { - this.emit('createEvent', record); - }); - - return record; - } - - createEventForPeers(type, payload, peers, options = {}) { - const records = []; - - for (const peer of peers) { - const record = this.createEvent(type, payload, peer, options); - - records.push(record); +export const contextDefinitions = { + ROOM: { + type: 'room', + isRoom(event) { + return !!event.context.roomId; + }, + contextQuery(roomId) { + return { roomId }; + }, + }, + + defineType(event) { + if (this.ROOM.isRoom(event)) { + return this.ROOM.type; } - return records; - } - - // Create a `ping(png)` event - ping(peers) { - return this.createEventForPeers('png', {}, peers, { retry: { total: 1 } }); - } - - // Create a `directRoomCreated(drc)` event - directRoomCreated(federatedRoom, options = {}) { - const peers = normalizePeers(federatedRoom.getPeers(), options); - - const payload = { - room: federatedRoom.getRoom(), - owner: federatedRoom.getOwner(), - users: federatedRoom.getUsers(), - }; - - return this.createEventForPeers('drc', payload, peers); - } - - // Create a `roomCreated(roc)` event - roomCreated(federatedRoom, options = {}) { - const peers = normalizePeers(federatedRoom.getPeers(), options); - - const payload = { - room: federatedRoom.getRoom(), - owner: federatedRoom.getOwner(), - users: federatedRoom.getUsers(), - }; - - return this.createEventForPeers('roc', payload, peers); - } - - // Create a `userJoined(usj)` event - userJoined(federatedRoom, federatedUser, options = {}) { - const peers = normalizePeers(federatedRoom.getPeers(), options); - - const payload = { - federated_room_id: federatedRoom.getFederationId(), - user: federatedUser.getUser(), - }; - - return this.createEventForPeers('usj', payload, peers); - } - - // Create a `userAdded(usa)` event - userAdded(federatedRoom, federatedUser, federatedInviter, options = {}) { - const peers = normalizePeers(federatedRoom.getPeers(), options); + return 'undefined'; + }, +}; - const payload = { - federated_room_id: federatedRoom.getFederationId(), - federated_inviter_id: federatedInviter.getFederationId(), - user: federatedUser.getUser(), - }; +export class FederationEventsModel extends Base { + constructor(nameOrModel) { + super(nameOrModel); - return this.createEventForPeers('usa', payload, peers); + this.tryEnsureIndex({ hasChildren: 1 }, { sparse: true }); + this.tryEnsureIndex({ timestamp: 1 }); } - // Create a `userLeft(usl)` event - userLeft(federatedRoom, federatedUser, options = {}) { - const peers = normalizePeers(federatedRoom.getPeers(), options); - - const payload = { - federated_room_id: federatedRoom.getFederationId(), - federated_user_id: federatedUser.getFederationId(), - }; - - return this.createEventForPeers('usl', payload, peers); + getEventHash(contextQuery, event) { + return SHA256(`${ event.origin }${ JSON.stringify(contextQuery) }${ event.parentIds.join(',') }${ event.type }${ event.timestamp }${ JSON.stringify(event.data) }`); } - // Create a `userRemoved(usr)` event - userRemoved(federatedRoom, federatedUser, federatedRemovedByUser, options = {}) { - const peers = normalizePeers(federatedRoom.getPeers(), options); - - const payload = { - federated_room_id: federatedRoom.getFederationId(), - federated_user_id: federatedUser.getFederationId(), - federated_removed_by_user_id: federatedRemovedByUser.getFederationId(), - }; - - return this.createEventForPeers('usr', payload, peers); - } + async createEvent(origin, contextQuery, type, data) { + let previousEventsIds = []; - // Create a `userMuted(usm)` event - userMuted(federatedRoom, federatedUser, federatedMutedByUser, options = {}) { - const peers = normalizePeers(federatedRoom.getPeers(), options); + // If it is not a GENESIS event, we need to get the previous events + if (type !== eventTypes.GENESIS) { + const previousEvents = await this.model + .rawCollection() + .find({ context: contextQuery, hasChildren: false }) + .toArray(); - const payload = { - federated_room_id: federatedRoom.getFederationId(), - federated_user_id: federatedUser.getFederationId(), - federated_muted_by_user_id: federatedMutedByUser.getFederationId(), - }; + // if (!previousEvents.length) { + // throw new Error('Could not create event, the context does not exist'); + // } - return this.createEventForPeers('usm', payload, peers); - } - - // Create a `userUnmuted(usu)` event - userUnmuted(federatedRoom, federatedUser, federatedUnmutedByUser, options = {}) { - const peers = normalizePeers(federatedRoom.getPeers(), options); + previousEventsIds = previousEvents.map((e) => e._id); + } - const payload = { - federated_room_id: federatedRoom.getFederationId(), - federated_user_id: federatedUser.getFederationId(), - federated_unmuted_by_user_id: federatedUnmutedByUser.getFederationId(), + const event = { + origin, + context: contextQuery, + parentIds: previousEventsIds || [], + type, + timestamp: new Date(), + data, + hasChildren: false, }; - return this.createEventForPeers('usu', payload, peers); - } + event._id = this.getEventHash(contextQuery, event); - // Create a `messageCreated(msc)` event - messageCreated(federatedRoom, federatedMessage, options = {}) { - const peers = normalizePeers(federatedRoom.getPeers(), options); + // this.insert(event); - const payload = { - message: federatedMessage.getMessage(), - }; + // Clear the "hasChildren" of those events + await this.update({ _id: { $in: previousEventsIds } }, { $unset: { hasChildren: '' } }, { multi: 1 }); - return this.createEventForPeers('msc', payload, peers); + return event; } - // Create a `messageUpdated(msu)` event - messageUpdated(federatedRoom, federatedMessage, federatedUser, options = {}) { - const peers = normalizePeers(federatedRoom.getPeers(), options); + async createGenesisEvent(origin, contextQuery, data) { + // Check if genesis event already exists, if so, do not create + const genesisEvent = await this.model + .rawCollection() + .findOne({ context: contextQuery, type: eventTypes.GENESIS }); - const payload = { - message: federatedMessage.getMessage(), - federated_user_id: federatedUser.getFederationId(), - }; + if (genesisEvent) { + throw new Error(`A GENESIS event for this context query already exists: ${ JSON.stringify(contextQuery, null, 2) }`); + } - return this.createEventForPeers('msu', payload, peers); + return this.createEvent(origin, contextQuery, eventTypes.GENESIS, data); } - // Create a `deleteMessage(msd)` event - messageDeleted(federatedRoom, federatedMessage, options = {}) { - const peers = normalizePeers(federatedRoom.getPeers(), options); + async addEvent(contextQuery, event) { + // Check if the event does not exit + const existingEvent = this.findOne({ _id: event._id }); - const payload = { - federated_message_id: federatedMessage.getFederationId(), - }; + // If it does not, we insert it, checking for the parents + if (!existingEvent) { + // Check if we have the parents + const parents = await this.model.rawCollection().find({ context: contextQuery, _id: { $in: event.parentIds } }, { _id: 1 }).toArray(); + const parentIds = parents.map(({ _id }) => _id); - return this.createEventForPeers('msd', payload, peers); - } + // This means that we do not have the parents of the event we are adding + if (parentIds.length !== event.parentIds.length) { + const { origin } = event; - // Create a `messagesRead(msr)` event - messagesRead(federatedRoom, federatedUser, options = {}) { - const peers = normalizePeers(federatedRoom.getPeers(), options); + // Get the latest events for that context and origin + const latestEvents = await this.model.rawCollection().find({ context: contextQuery, origin }, { _id: 1 }).toArray(); + const latestEventIds = latestEvents.map(({ _id }) => _id); - const payload = { - federated_room_id: federatedRoom.getFederationId(), - federated_user_id: federatedUser.getFederationId(), - }; + return { + success: false, + reason: 'missingParents', + missingParentIds: event.parentIds.filter(({ _id }) => parentIds.indexOf(_id) === -1), + latestEventIds, + }; + } - return this.createEventForPeers('msr', payload, peers); - } + // Clear the "hasChildren" of the parent events + await this.update({ _id: { $in: parentIds } }, { $unset: { hasChildren: '' } }, { multi: 1 }); - // Create a `messagesSetReaction(mrs)` event - messagesSetReaction(federatedRoom, federatedMessage, federatedUser, reaction, shouldReact, options = {}) { - const peers = normalizePeers(federatedRoom.getPeers(), options); + this.insert(event); + } - const payload = { - federated_room_id: federatedRoom.getFederationId(), - federated_message_id: federatedMessage.getFederationId(), - federated_user_id: federatedUser.getFederationId(), - reaction, - shouldReact, + return { + success: true, }; - - return this.createEventForPeers('mrs', payload, peers); } - // Create a `messagesUnsetReaction(mru)` event - messagesUnsetReaction(federatedRoom, federatedMessage, federatedUser, reaction, shouldReact, options = {}) { - const peers = normalizePeers(federatedRoom.getPeers(), options); + async getEventById(contextQuery, eventId) { + const event = await this.model + .rawCollection() + .findOne({ context: contextQuery, _id: eventId }); - const payload = { - federated_room_id: federatedRoom.getFederationId(), - federated_message_id: federatedMessage.getFederationId(), - federated_user_id: federatedUser.getFederationId(), - reaction, - shouldReact, + return { + success: !!event, + event, }; - - return this.createEventForPeers('mru', payload, peers); } - // Get all unfulfilled events - getUnfulfilled() { - return this.find({ fulfilled: false }, { sort: { ts: 1 } }); + async getLatestEvents(contextQuery, fromTimestamp) { + return this.model.rawCollection().find({ context: contextQuery, timestamp: { $gt: new Date(fromTimestamp) } }).toArray(); } - findByType(t) { - return this.find({ t }); + async removeContextEvents(contextQuery) { + return this.model.rawCollection().remove({ context: contextQuery }); } } - -export const FederationEvents = new FederationEventsModel(); diff --git a/app/models/server/models/FederationKeys.js b/app/models/server/models/FederationKeys.js index 8e2e9c26756..188f7cdc434 100644 --- a/app/models/server/models/FederationKeys.js +++ b/app/models/server/models/FederationKeys.js @@ -1,5 +1,4 @@ import NodeRSA from 'node-rsa'; -import uuid from 'uuid/v4'; import { Base } from './_Base'; @@ -35,16 +34,6 @@ class FederationKeysModel extends Base { }; } - generateUniqueId() { - const uniqueId = uuid(); - - this.update({ type: 'unique' }, { type: 'unique', key: uniqueId }, { upsert: true }); - } - - getUniqueId() { - return (this.findOne({ type: 'unique' }) || {}).key; - } - getPrivateKey() { const keyData = this.getKey('private'); diff --git a/app/models/server/models/FederationPeers.js b/app/models/server/models/FederationPeers.js deleted file mode 100644 index 283d2f31e76..00000000000 --- a/app/models/server/models/FederationPeers.js +++ /dev/null @@ -1,60 +0,0 @@ -import { Base } from './_Base'; -import { Users } from '../raw'; - -class FederationPeersModel extends Base { - constructor() { - super('federation_peers'); - - this.tryEnsureIndex({ active: 1, isRemote: 1 }); - } - - async refreshPeers(localIdentifier) { - const peers = await Users.getDistinctFederationPeers(); - - peers.forEach((peer) => - this.update({ peer }, { - $setOnInsert: { - isRemote: localIdentifier !== peer, - active: false, - peer, - last_seen_at: null, - last_failure_at: null, - }, - }, { upsert: true }) - ); - - this.remove({ peer: { $nin: peers } }); - } - - updateStatuses(seenPeers) { - for (const peer of Object.keys(seenPeers)) { - const seen = seenPeers[peer]; - - const updateQuery = {}; - - if (seen) { - updateQuery.active = true; - updateQuery.last_seen_at = new Date(); - } else { - updateQuery.active = false; - updateQuery.last_failure_at = new Date(); - } - - this.update({ peer }, { $set: updateQuery }); - } - } - - findActiveRemote() { - return this.find({ active: true, isRemote: true }); - } - - findNotActiveRemote() { - return this.find({ active: false, isRemote: true }); - } - - findRemote() { - return this.find({ isRemote: true }); - } -} - -export const FederationPeers = new FederationPeersModel(); diff --git a/app/models/server/models/FederationRoomEvents.js b/app/models/server/models/FederationRoomEvents.js new file mode 100644 index 00000000000..a7b375fdd45 --- /dev/null +++ b/app/models/server/models/FederationRoomEvents.js @@ -0,0 +1,67 @@ +import { FederationEventsModel, contextDefinitions, eventTypes } from './FederationEvents'; + +const { type, contextQuery } = contextDefinitions.ROOM; + +class FederationRoomEventsModel extends FederationEventsModel { + constructor() { + super('federation_room_events'); + + this.tryEnsureIndex({ 'context.roomId': 1 }); + } + + async createGenesisEvent(origin, room) { + return super.createGenesisEvent(origin, contextQuery(room._id), { contextType: type, room }); + } + + async createDeleteRoomEvent(origin, roomId) { + return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_DELETE, { roomId }); + } + + async createAddUserEvent(origin, roomId, user, subscription, domainsAfterAdd) { + return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_ADD_USER, { roomId, user, subscription, domainsAfterAdd }); + } + + async createRemoveUserEvent(origin, roomId, user, domainsAfterRemoval) { + return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_REMOVE_USER, { roomId, user, domainsAfterRemoval }); + } + + async createMessageEvent(origin, roomId, message) { + return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_MESSAGE, { message }); + } + + async createEditMessageEvent(origin, roomId, originalMessage) { + const message = { + _id: originalMessage._id, + msg: originalMessage.msg, + federation: originalMessage.federation, + }; + + return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_EDIT_MESSAGE, { message }); + } + + async createDeleteMessageEvent(origin, roomId, messageId) { + return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_DELETE_MESSAGE, { roomId, messageId }); + } + + async createSetMessageReactionEvent(origin, roomId, messageId, username, reaction) { + return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_SET_MESSAGE_REACTION, { roomId, messageId, username, reaction }); + } + + async createUnsetMessageReactionEvent(origin, roomId, messageId, username, reaction) { + return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_UNSET_MESSAGE_REACTION, { roomId, messageId, username, reaction }); + } + + async createMuteUserEvent(origin, roomId, user) { + return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_MUTE_USER, { roomId, user }); + } + + async createUnmuteUserEvent(origin, roomId, user) { + return super.createEvent(origin, contextQuery(roomId), eventTypes.ROOM_UNMUTE_USER, { roomId, user }); + } + + async removeRoomEvents(roomId) { + return super.removeContextEvents(contextQuery(roomId)); + } +} + +export const FederationRoomEvents = new FederationRoomEventsModel(); diff --git a/app/models/server/models/FederationServers.js b/app/models/server/models/FederationServers.js new file mode 100644 index 00000000000..9daf20d5a12 --- /dev/null +++ b/app/models/server/models/FederationServers.js @@ -0,0 +1,26 @@ +import { Base } from './_Base'; +import { Users } from '../raw'; + +class FederationServersModel extends Base { + constructor() { + super('federation_servers'); + + this.tryEnsureIndex({ domain: 1 }); + } + + async refreshServers() { + const domains = await Users.getDistinctFederationDomains(); + + domains.forEach((domain) => { + this.update({ domain }, { + $setOnInsert: { + domain, + }, + }, { upsert: true }); + }); + + this.remove({ domain: { $nin: domains } }); + } +} + +export const FederationServers = new FederationServersModel(); diff --git a/app/models/server/models/Messages.js b/app/models/server/models/Messages.js index 64e40d15729..0489bc85226 100644 --- a/app/models/server/models/Messages.js +++ b/app/models/server/models/Messages.js @@ -850,16 +850,6 @@ export class Messages extends Base { return this.createWithTypeRoomIdMessageAndUser('subscription-role-removed', roomId, message, user, extraData); } - createRejectedMessageByPeer(roomId, user, extraData) { - const message = user.username; - return this.createWithTypeRoomIdMessageAndUser('rejected-message-by-peer', roomId, message, user, extraData); - } - - createPeerDoesNotExist(roomId, user, extraData) { - const message = user.username; - return this.createWithTypeRoomIdMessageAndUser('peer-does-not-exist', roomId, message, user, extraData); - } - // REMOVE removeById(_id) { const query = { _id }; diff --git a/app/models/server/models/Users.js b/app/models/server/models/Users.js index cfcda4d4f0b..6ed4ff8b85b 100644 --- a/app/models/server/models/Users.js +++ b/app/models/server/models/Users.js @@ -555,22 +555,22 @@ export class Users extends Base { return this._db.find(query, options); } - findByActiveLocalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localPeer) { + findByActiveLocalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localDomain) { const extraQuery = [ { $or: [ { federation: { $exists: false } }, - { 'federation.peer': localPeer }, + { 'federation.origin': localDomain }, ], }, ]; return this.findByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery); } - findByActiveExternalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localPeer) { + findByActiveExternalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localDomain) { const extraQuery = [ { federation: { $exists: true } }, - { 'federation.peer': { $ne: localPeer } }, + { 'federation.origin': { $ne: localDomain } }, ]; return this.findByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery); } diff --git a/app/models/server/raw/Users.js b/app/models/server/raw/Users.js index ff9077f9e14..a9bffcdaff8 100644 --- a/app/models/server/raw/Users.js +++ b/app/models/server/raw/Users.js @@ -22,8 +22,8 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, { fields: { roles: 1 } }); } - getDistinctFederationPeers() { - return this.col.distinct('federation.peer', { federation: { $exists: true } }); + getDistinctFederationDomains() { + return this.col.distinct('federation.origin', { federation: { $exists: true } }); } async getNextLeastBusyAgent(department) { diff --git a/app/statistics/server/functions/get.js b/app/statistics/server/functions/get.js index 32c8801827b..2501754e680 100644 --- a/app/statistics/server/functions/get.js +++ b/app/statistics/server/functions/get.js @@ -20,7 +20,7 @@ import { Info, getMongoInfo } from '../../../utils/server'; import { Migrations } from '../../../migrations/server'; import { statistics } from '../statisticsNamespace'; import { Apps } from '../../../apps/server'; -import { getStatistics as federationGetStatistics } from '../../../federation/server/methods/dashboard'; +import { getStatistics as federationGetStatistics } from '../../../federation/server/functions/dashboard'; const wizardFields = [ 'Organization_Type', @@ -94,8 +94,7 @@ statistics.get = function _getStatistics() { // Federation statistics const federationOverviewData = federationGetStatistics(); - statistics.federatedServers = federationOverviewData.numberOfActivePeers + federationOverviewData.numberOfInactivePeers; - statistics.federatedServersActive = federationOverviewData.numberOfActivePeers; + statistics.federatedServers = federationOverviewData.numberOfServers; statistics.federatedUsers = federationOverviewData.numberOfFederatedUsers; statistics.lastLogin = Users.getLastLogin(); diff --git a/app/ui/client/views/app/directory.html b/app/ui/client/views/app/directory.html index a209293b1fc..fc22abf3aac 100644 --- a/app/ui/client/views/app/directory.html +++ b/app/ui/client/views/app/directory.html @@ -113,13 +113,15 @@ {{/if}} {{#if $eq searchWorkspace 'external'}} - -
{{_ "Domain"}} {{> icon icon=(sortIcon 'domain') }}
+ +
{{_ "Domain"}} {{> icon icon=(sortIcon 'origin') }}
{{/if}} + {{#if $eq searchWorkspace 'internal'}}
{{_ "Created_at"}} {{> icon icon=(sortIcon 'createdAt') }}
+ {{/if}} diff --git a/app/ui/client/views/app/directory.js b/app/ui/client/views/app/directory.js index ba4af117e7f..73299732aa5 100644 --- a/app/ui/client/views/app/directory.js +++ b/app/ui/client/views/app/directory.js @@ -33,7 +33,8 @@ function directorySearch(config, cb) { // If there is no email address (probably only rocket.cat) show the username) email: (result.emails && result.emails[0] && result.emails[0].address) || result.username, createdAt: timeAgo(result.createdAt, t), - domain: result.federation && result.federation.peer, + origin: result.federation && result.federation.origin, + isRemote: result.isRemote, }; } return null; diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index badfca41d0e..a85695a322e 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1368,12 +1368,14 @@ "FEDERATION_Hub_URL_Description": "Set the hub URL, for example: https://hub.rocket.chat. Ports are accepted as well.", "FEDERATION_Public_Key": "Public Key", "FEDERATION_Public_Key_Description": "This is the key you need to share with your peers.", + "FEDERATION_Room_Status": "Federation Status", "FEDERATION_Status": "Status", "FEDERATION_Test_Setup": "Test setup", "FEDERATION_Test_Setup_Error": "Could not find your server using your setup, please review your settings.", "FEDERATION_Test_Setup_Success": "Your federation setup is working and other servers can find you!", "FEDERATION_Unique_Id": "Unique ID", "FEDERATION_Unique_Id_Description": "This is your federation unique ID, used to identify your peer on the mesh.", + "FEDERATION_Error_user_is_federated_on_rooms": "You can't remove federated users who belongs to rooms", "Field": "Field", "Field_removed": "Field removed", "Field_required": "Field required", @@ -2277,10 +2279,9 @@ "Notify_active_in_this_room": "Notify active users in this room", "Notify_all_in_this_room": "Notify all in this room", "Num_Agents": "# Agents", - "Number_of_active_peers": "Number of active peers", "Number_of_events": "Number of events", "Number_of_federated_users": "Number of federated users", - "Number_of_inactive_peers": "Number of inactive peers", + "Number_of_federated_servers": "Number of federated servers", "Number_of_messages": "Number of messages", "OAuth Apps": "OAuth Apps", "OAuth_Application": "OAuth Application", diff --git a/server/methods/browseChannels.js b/server/methods/browseChannels.js index 64ab04e377d..25dd440b665 100644 --- a/server/methods/browseChannels.js +++ b/server/methods/browseChannels.js @@ -4,8 +4,10 @@ import s from 'underscore.string'; import { hasPermission } from '../../app/authorization'; import { Rooms, Users } from '../../app/models'; -import { Federation } from '../../app/federation/server'; import { settings } from '../../app/settings/server'; +import { getFederationDomain } from '../../app/federation/server/lib/getFederationDomain'; +import { isFederationEnabled } from '../../app/federation/server/lib/isFederationEnabled'; +import { federationSearchUsers } from '../../app/federation/server/handler'; const sortChannels = function(field, direction) { switch (field) { @@ -118,32 +120,28 @@ Meteor.methods({ if (workspace === 'all') { result = Users.findByActiveUsersExcept(text, exceptions, options, forcedSearchFields); } else if (workspace === 'external') { - result = Users.findByActiveExternalUsersExcept(text, exceptions, options, forcedSearchFields, Federation.localIdentifier); + result = Users.findByActiveExternalUsersExcept(text, exceptions, options, forcedSearchFields, getFederationDomain()); } else { - result = Users.findByActiveLocalUsersExcept(text, exceptions, options, forcedSearchFields, Federation.localIdentifier); + result = Users.findByActiveLocalUsersExcept(text, exceptions, options, forcedSearchFields, getFederationDomain()); } const total = result.count(); // count ignores the `skip` and `limit` options const results = result.fetch(); // Try to find federated users, when appliable - if (Federation.enabled && type === 'users' && workspace === 'external' && text.indexOf('@') !== -1) { - const federatedUsers = Federation.methods.searchUsers(text); + if (isFederationEnabled() && type === 'users' && workspace === 'external' && text.indexOf('@') !== -1) { + const users = federationSearchUsers(text); - for (const federatedUser of federatedUsers) { - const { user } = federatedUser; - - const exists = results.findIndex((e) => e.domain === user.federation.peer && e.username === user.username) !== -1; - - if (exists) { continue; } + for (const user of users) { + if (results.find((e) => e._id === user._id)) { continue; } // Add the federated user to the results results.unshift({ username: user.username, name: user.name, - createdAt: user.createdAt, emails: user.emails, federation: user.federation, + isRemote: true, }); } } diff --git a/server/methods/createDirectMessage.js b/server/methods/createDirectMessage.js index 2fb89543598..c2d2dcc9d17 100644 --- a/server/methods/createDirectMessage.js +++ b/server/methods/createDirectMessage.js @@ -7,7 +7,7 @@ import { Users, Rooms, Subscriptions } from '../../app/models'; import { getDefaultSubscriptionPref } from '../../app/utils'; import { RateLimiter } from '../../app/lib'; import { callbacks } from '../../app/callbacks'; -import { Federation } from '../../app/federation/server'; +import { addUser } from '../../app/federation/server/functions/addUser'; Meteor.methods({ createDirectMessage(username) { @@ -41,11 +41,9 @@ Meteor.methods({ let to = Users.findOneByUsernameIgnoringCase(username); + // If the username does have an `@`, but does not exist locally, we create it first if (!to && username.indexOf('@') !== -1) { - // If the username does have an `@`, but does not exist locally, we create it first - const toId = Federation.methods.addUser(username); - - to = Users.findOneById(toId); + to = addUser(username); } if (!to) { diff --git a/server/startup/migrations/v143.js b/server/startup/migrations/v143.js index 15f93b2b0bf..1251c7a6002 100644 --- a/server/startup/migrations/v143.js +++ b/server/startup/migrations/v143.js @@ -1,5 +1,5 @@ import { Migrations } from '../../../app/migrations/server'; -import { Users, FederationPeers } from '../../../app/models/server'; +import { Users, FederationServers } from '../../../app/models/server'; Migrations.add({ version: 143, @@ -15,7 +15,7 @@ Migrations.add({ })); if (peers.length) { - FederationPeers.model.rawCollection().insertMany(peers); + FederationServers.model.rawCollection().insertMany(peers); } }, }); diff --git a/server/startup/migrations/v148.js b/server/startup/migrations/v148.js index a72665351dd..62740cdd2d0 100644 --- a/server/startup/migrations/v148.js +++ b/server/startup/migrations/v148.js @@ -1,5 +1,5 @@ import { Migrations } from '../../../app/migrations/server'; -import { Users, Settings, FederationPeers } from '../../../app/models/server'; +import { Users, Settings, FederationServers } from '../../../app/models/server'; Migrations.add({ version: 148, @@ -10,7 +10,9 @@ Migrations.add({ return; } - const { value: localDomain } = domainSetting; + const { value: domain } = domainSetting; + + const localDomain = domain.replace('@', ''); Users.update({ federation: { $exists: true }, 'federation.peer': { $ne: localDomain }, @@ -18,13 +20,13 @@ Migrations.add({ $set: { isRemote: true }, }, { multi: true }); - FederationPeers.update({ + FederationServers.update({ peer: { $ne: localDomain }, }, { $set: { isRemote: true }, }, { multi: true }); - FederationPeers.update({ + FederationServers.update({ peer: localDomain, }, { $set: { isRemote: false },