From 661edcae485867a3b1abad1e78a06b123e7064b3 Mon Sep 17 00:00:00 2001 From: Alan Sikora Date: Thu, 28 Mar 2019 14:16:50 -0300 Subject: [PATCH 1/8] Better federation file and code organization --- app/federation/server/PeerClient.js | 611 ++++++++++++++++++ app/federation/server/PeerDNS.js | 171 +++++ app/federation/server/PeerHTTP/PeerHTTP.js | 100 +++ app/federation/server/PeerHTTP/index.js | 1 + app/federation/server/PeerHTTP/utils.js | 19 + .../server/PeerServer/PeerServer.js | 386 +++++++++++ app/federation/server/PeerServer/index.js | 6 + .../server/PeerServer/routes/events.js | 103 +++ .../server/PeerServer/routes/uploads.js | 28 + .../server/PeerServer/routes/users.js | 49 ++ .../federatedResources/FederatedMessage.js | 15 +- .../federatedResources/FederatedResource.js | 4 +- .../federatedResources/FederatedRoom.js | 14 +- .../federatedResources/FederatedUser.js | 6 +- .../server/federatedResources/index.js | 8 +- app/federation/server/index.js | 28 +- app/federation/server/methods/addUser.js | 44 ++ .../server/methods/federationAddUser.js | 48 -- .../server/methods/federationSearchUsers.js | 24 - app/federation/server/methods/index.js | 2 - app/federation/server/methods/searchUsers.js | 19 + app/federation/server/peerClient.js | 28 +- app/federation/server/peerDNS.js | 10 +- app/federation/server/peerHTTP.js | 126 ---- app/federation/server/peerServer/index.js | 4 +- .../server/peerServer/peerServer.js | 28 +- .../server/peerServer/routes/events.js | 41 +- .../server/peerServer/routes/uploads.js | 4 +- .../server/peerServer/routes/users.js | 14 +- 29 files changed, 1638 insertions(+), 303 deletions(-) create mode 100644 app/federation/server/PeerClient.js create mode 100644 app/federation/server/PeerDNS.js create mode 100644 app/federation/server/PeerHTTP/PeerHTTP.js create mode 100644 app/federation/server/PeerHTTP/index.js create mode 100644 app/federation/server/PeerHTTP/utils.js create mode 100644 app/federation/server/PeerServer/PeerServer.js create mode 100644 app/federation/server/PeerServer/index.js create mode 100644 app/federation/server/PeerServer/routes/events.js create mode 100644 app/federation/server/PeerServer/routes/uploads.js create mode 100644 app/federation/server/PeerServer/routes/users.js create mode 100644 app/federation/server/methods/addUser.js delete mode 100644 app/federation/server/methods/federationAddUser.js delete mode 100644 app/federation/server/methods/federationSearchUsers.js delete mode 100644 app/federation/server/methods/index.js create mode 100644 app/federation/server/methods/searchUsers.js delete mode 100644 app/federation/server/peerHTTP.js diff --git a/app/federation/server/PeerClient.js b/app/federation/server/PeerClient.js new file mode 100644 index 000000000000..6639d2d3a3b7 --- /dev/null +++ b/app/federation/server/PeerClient.js @@ -0,0 +1,611 @@ +import qs from 'querystring'; +import { Meteor } from 'meteor/meteor'; +import { callbacks } from '../../callbacks'; +import { settings } from '../../settings'; +import { FederationEvents, FederationKeys, Messages, Rooms, Subscriptions, Users } from '../../models'; + +import { updateStatus } from './settingsUpdater'; +import { logger } from './logger'; +import { FederatedMessage, FederatedRoom, FederatedUser } from './federatedResources'; +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-join-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-join-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-leave-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 } = 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 }, { 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(); + + for (const e of events) { + 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] }); + } + + 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] }); + } + + 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 is federated + if (!FederatedRoom.isFederated(localPeerDomain, room, { checkUsingUsers: true })) { 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] }); + } + } + + 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(); + } + + 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(); + } + + 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] }); + } + } + + 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] }); + } + + 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] }); + } + + 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] }); + } + + 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] }); + } + + 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] }); + } + + 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] }); + } +} diff --git a/app/federation/server/PeerDNS.js b/app/federation/server/PeerDNS.js new file mode 100644 index 000000000000..1959cfa6ab11 --- /dev/null +++ b/app/federation/server/PeerDNS.js @@ -0,0 +1,171 @@ +import dns from 'dns'; +import { Meteor } from 'meteor/meteor'; +import { FederationDNSCache } from '../../models'; + +import { logger } from './logger'; +import { updateStatus } from './settingsUpdater'; +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) { + this.updatePeerDNS(domain); + + peer = FederationDNSCache.findOneByDomain(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 public key from the TXT record + const txtRecords = dnsResolveTXT(`rocketchat-public-key.${ domain }`); + + // Get the first TXT record, this subdomain should have only a single record + const txtRecord = txtRecords[0]; + + // If there is no record, skip + if (!txtRecord) { + throw new Meteor.Error('ENOTFOUND', 'Could not find public key entry on TXT records'); + } + + const publicKey = txtRecord.join(''); + + const protocol = srvEntry.name === 'localhost' ? 'http' : 'https'; + + 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; + + 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 }`); + } + + peer = this.getPeerUsingHub(domain); + } + + 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 new file mode 100644 index 000000000000..f45e59eb0706 --- /dev/null +++ b/app/federation/server/PeerHTTP/PeerHTTP.js @@ -0,0 +1,100 @@ +import { Meteor } from 'meteor/meteor'; +import { HTTP } from 'meteor/http'; + +import { logger } from '../logger'; +import { Federation } from '../'; + +import { skipRetryOnSpecificError, delay } from './utils'; + +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) { + this.log(`Request: ${ method } ${ uri }`); + + const { url: serverBaseURL } = peer; + + const url = `${ serverBaseURL }${ uri }`; + + let data = null; + + if (method === 'POST' || method === 'PUT') { + data = body; + } + + this.log(`Sending request: ${ method } - ${ uri }`); + + 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 + if (skipRetryOnSpecificError(err)) { + this.log('Retry: skipping due to specific error'); + + 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 new file mode 100644 index 000000000000..3c9e957f1cc9 --- /dev/null +++ b/app/federation/server/PeerHTTP/index.js @@ -0,0 +1 @@ +export { PeerHTTP } from './PeerHTTP'; diff --git a/app/federation/server/PeerHTTP/utils.js b/app/federation/server/PeerHTTP/utils.js new file mode 100644 index 000000000000..a3d5d153d612 --- /dev/null +++ b/app/federation/server/PeerHTTP/utils.js @@ -0,0 +1,19 @@ +import { Meteor } from 'meteor/meteor'; + +// Should skip the retry if the error is one of the below? +const errorsToSkipRetrying = [ + 'error-app-prevented-sending', +]; + +export function skipRetryOnSpecificError(err) { + err = err && err.response && err.response.data; + return errorsToSkipRetrying.includes(err && err.errorType); +} + +// 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/PeerServer/PeerServer.js b/app/federation/server/PeerServer/PeerServer.js new file mode 100644 index 000000000000..6f7e8f84f1ba --- /dev/null +++ b/app/federation/server/PeerServer/PeerServer.js @@ -0,0 +1,386 @@ +import { callbacks } from '../../../callbacks'; +import { setReaction } from '../../../reactions/server'; +import { addUserToRoom, removeUserFromRoom, deleteMessage } from '../../../lib'; +import { Rooms, Subscriptions } 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(); + } + + 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); + } + + 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(); + } + + 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(); + } + + 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(); + } + + 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(); + } + + 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 new file mode 100644 index 000000000000..e1da97c3327a --- /dev/null +++ b/app/federation/server/PeerServer/index.js @@ -0,0 +1,6 @@ +// 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 new file mode 100644 index 000000000000..f8703a6d0dec --- /dev/null +++ b/app/federation/server/PeerServer/routes/events.js @@ -0,0 +1,103 @@ +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); + + // Decrypt with the peer's public key + let payload = FederationKeys.loadKey(peer.public_key, 'public').decryptPublic(payloadBuffer); + + // Decrypt with the local private key + payload = Federation.privateKey.decrypt(payload); + + // 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 '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) { + Federation.peerServer.log(`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/uploads.js b/app/federation/server/PeerServer/routes/uploads.js new file mode 100644 index 000000000000..696a10096529 --- /dev/null +++ b/app/federation/server/PeerServer/routes/uploads.js @@ -0,0 +1,28 @@ +import { Meteor } from 'meteor/meteor'; +import { API } from '../../../../api'; +import { Uploads } from '../../../../models'; +import { FileUpload } from '../../../../file-upload'; + +import { Federation } from '../../'; + +API.v1.addRoute('federation.uploads', { authRequired: false }, { + get() { + if (!Federation.peerServer.enabled) { + return API.v1.failure('Not found'); + } + + const { upload_id } = this.requestParams(); + + const upload = Uploads.findOneById(upload_id); + + if (!upload) { + return API.v1.failure('There is no such file in this server'); + } + + const getFileBuffer = Meteor.wrapAsync(FileUpload.getBuffer, FileUpload); + + const buffer = getFileBuffer(upload); + + return API.v1.success({ upload, buffer }); + }, +}); diff --git a/app/federation/server/PeerServer/routes/users.js b/app/federation/server/PeerServer/routes/users.js new file mode 100644 index 000000000000..4e95e5fd5f33 --- /dev/null +++ b/app/federation/server/PeerServer/routes/users.js @@ -0,0 +1,49 @@ +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/federatedResources/FederatedMessage.js b/app/federation/server/federatedResources/FederatedMessage.js index dfd9bb860559..97cb4c273c19 100644 --- a/app/federation/server/federatedResources/FederatedMessage.js +++ b/app/federation/server/federatedResources/FederatedMessage.js @@ -3,12 +3,12 @@ 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 peerClient from '../peerClient'; +import { FederatedResource } from './FederatedResource'; +import { FederatedRoom } from './FederatedRoom'; +import { FederatedUser } from './FederatedUser'; +import { Federation } from '../'; -class FederatedMessage extends FederatedResource { +export class FederatedMessage extends FederatedResource { constructor(localPeerIdentifier, message) { super('message'); @@ -16,6 +16,7 @@ class FederatedMessage extends FederatedResource { throw new Error('message param cannot be empty'); } + // Set local peer identifier to local object this.localPeerIdentifier = localPeerIdentifier; // Make sure room dates are correct @@ -178,7 +179,7 @@ class FederatedMessage extends FederatedResource { const { federation: { peer: identifier } } = localMessage; - const { upload, buffer } = peerClient.getUpload({ identifier, localMessage }); + const { upload, buffer } = Federation.peerClient.getUpload({ identifier, localMessage }); const oldUploadId = upload._id; @@ -260,5 +261,3 @@ FederatedMessage.loadOrCreate = function loadOrCreate(localPeerIdentifier, messa return new FederatedMessage(localPeerIdentifier, message); }; - -export default FederatedMessage; diff --git a/app/federation/server/federatedResources/FederatedResource.js b/app/federation/server/federatedResources/FederatedResource.js index 772dc30348b0..7ecdb9ec1cd3 100644 --- a/app/federation/server/federatedResources/FederatedResource.js +++ b/app/federation/server/federatedResources/FederatedResource.js @@ -1,6 +1,6 @@ import { logger } from '../logger'; -class FederatedResource { +export class FederatedResource { constructor(name) { this.resourceName = `federated-${ name }`; @@ -15,5 +15,3 @@ class FederatedResource { FederatedResource.log = function log(name, message) { logger.resource.info(`[${ name }] ${ message }`); }; - -export default FederatedResource; diff --git a/app/federation/server/federatedResources/FederatedRoom.js b/app/federation/server/federatedResources/FederatedRoom.js index ac1e7b47a1d8..06e5a0a3a855 100644 --- a/app/federation/server/federatedResources/FederatedRoom.js +++ b/app/federation/server/federatedResources/FederatedRoom.js @@ -1,10 +1,10 @@ import { createRoom } from '../../../lib'; import { Rooms, Subscriptions, Users } from '../../../models'; -import FederatedResource from './FederatedResource'; -import FederatedUser from './FederatedUser'; +import { FederatedResource } from './FederatedResource'; +import { FederatedUser } from './FederatedUser'; -class FederatedRoom extends FederatedResource { +export class FederatedRoom extends FederatedResource { constructor(localPeerIdentifier, room, extras = {}) { super('room'); @@ -144,7 +144,7 @@ class FederatedRoom extends FederatedResource { } } - create() { + create(alertAndOpen = false) { this.log('create'); // Get the local room object (with or without suffixes) @@ -195,8 +195,8 @@ class FederatedRoom extends FederatedResource { let createRoomOptions = { subscriptionExtra: { - alert: true, - open: true, + alert: alertAndOpen, + open: alertAndOpen, }, }; @@ -266,5 +266,3 @@ FederatedRoom.isFederated = function isFederated(localPeerIdentifier, room, opti return isFederated; }; - -export default FederatedRoom; diff --git a/app/federation/server/federatedResources/FederatedUser.js b/app/federation/server/federatedResources/FederatedUser.js index 41d431ddab83..74f40bbf3168 100644 --- a/app/federation/server/federatedResources/FederatedUser.js +++ b/app/federation/server/federatedResources/FederatedUser.js @@ -1,8 +1,8 @@ import { Users } from '../../../models'; -import FederatedResource from './FederatedResource'; +import { FederatedResource } from './FederatedResource'; -class FederatedUser extends FederatedResource { +export class FederatedUser extends FederatedResource { constructor(localPeerIdentifier, user) { super('user'); @@ -120,5 +120,3 @@ FederatedUser.loadOrCreate = function loadOrCreate(localPeerIdentifier, user) { return new FederatedUser(localPeerIdentifier, user); }; - -export default FederatedUser; diff --git a/app/federation/server/federatedResources/index.js b/app/federation/server/federatedResources/index.js index 0bd6fdd4cbb4..90d98b351cab 100644 --- a/app/federation/server/federatedResources/index.js +++ b/app/federation/server/federatedResources/index.js @@ -1,4 +1,4 @@ -export { default as FederatedMessage } from './FederatedMessage'; -export { default as FederatedResource } from './FederatedResource'; -export { default as FederatedRoom } from './FederatedRoom'; -export { default as FederatedUser } from './FederatedUser'; +export { FederatedMessage } from './FederatedMessage'; +export { FederatedResource } from './FederatedResource'; +export { FederatedRoom } from './FederatedRoom'; +export { FederatedUser } from './FederatedUser'; diff --git a/app/federation/server/index.js b/app/federation/server/index.js index 165b56d6b5e4..67afc5ce3081 100644 --- a/app/federation/server/index.js +++ b/app/federation/server/index.js @@ -5,15 +5,22 @@ import { FederationKeys } from '../../models'; import { getWorkspaceAccessToken } from '../../cloud/server'; import './federation-settings'; -import './methods'; import { logger } from './logger'; -import peerClient from './peerClient'; -import peerServer from './peerServer'; -import peerDNS from './peerDNS'; -import peerHTTP from './peerHTTP'; +import { PeerClient } from './PeerClient'; +import { PeerDNS } from './PeerDNS'; +import { PeerHTTP } from './PeerHTTP'; +import { PeerServer } from './PeerServer'; import * as SettingsUpdater from './settingsUpdater'; +import { addUser } from './methods/addUser'; +import { searchUsers } from './methods/searchUsers'; + +const peerClient = new PeerClient(); +const peerDNS = new PeerDNS(); +const peerHTTP = new PeerHTTP(); +const peerServer = new PeerServer(); + export const Federation = { enabled: false, privateKey: null, @@ -21,6 +28,17 @@ export const Federation = { usingHub: null, uniqueId: null, localIdentifier: null, + + peerClient, + peerDNS, + peerHTTP, + peerServer, +}; + +// Add Federation methods with bound context +Federation.methods = { + addUser: addUser.bind(Federation), + searchUsers: searchUsers.bind(Federation), }; // Generate keys diff --git a/app/federation/server/methods/addUser.js b/app/federation/server/methods/addUser.js new file mode 100644 index 000000000000..bd87b5ee6147 --- /dev/null +++ b/app/federation/server/methods/addUser.js @@ -0,0 +1,44 @@ +import { Meteor } from 'meteor/meteor'; +import { Users } from '../../../models'; + +import { logger } from '../logger'; + +export function addUser(identifier) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'Federation.addUser' }); + } + + if (!this.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] = this.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); + } 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/federationAddUser.js b/app/federation/server/methods/federationAddUser.js deleted file mode 100644 index c4c5b6012828..000000000000 --- a/app/federation/server/methods/federationAddUser.js +++ /dev/null @@ -1,48 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Users } from '../../../models'; - -import { logger } from '../logger'; -import peerClient from '../peerClient'; -import peerServer from '../peerClient'; - -Meteor.methods({ - federationAddUser(emailAddress, domainOverride) { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'federationAddUser' }); - } - - if (!peerServer.enabled) { - throw new Meteor.Error('error-federation-disabled', 'Federation disabled', { method: 'federationAddUser' }); - } - - // Make sure the federated user still exists, and get the unique one, by email address - const [federatedUser] = peerClient.findUsers(emailAddress, { domainOverride, emailOnly: 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); - } 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/federationSearchUsers.js b/app/federation/server/methods/federationSearchUsers.js deleted file mode 100644 index cad994083e34..000000000000 --- a/app/federation/server/methods/federationSearchUsers.js +++ /dev/null @@ -1,24 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import peerClient from '../peerClient'; -import peerServer from '../peerServer'; - -Meteor.methods({ - federationSearchUsers(email) { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'federationSearchUsers' }); - } - - if (!peerServer.enabled) { - throw new Meteor.Error('error-federation-disabled', 'Federation disabled', { method: 'federationAddUser' }); - } - - const federatedUsers = peerClient.findUsers(email); - - if (!federatedUsers.length) { - throw new Meteor.Error('federation-user-not-found', `Could not find federated users using "${ email }"`); - } - - return federatedUsers; - }, -}); diff --git a/app/federation/server/methods/index.js b/app/federation/server/methods/index.js deleted file mode 100644 index ce95109beb42..000000000000 --- a/app/federation/server/methods/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import './federationSearchUsers'; -import './federationAddUser'; diff --git a/app/federation/server/methods/searchUsers.js b/app/federation/server/methods/searchUsers.js new file mode 100644 index 000000000000..5dade6646f75 --- /dev/null +++ b/app/federation/server/methods/searchUsers.js @@ -0,0 +1,19 @@ +import { Meteor } from 'meteor/meteor'; + +export function searchUsers(identifier) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'federationSearchUsers' }); + } + + if (!this.peerClient.enabled) { + throw new Meteor.Error('error-federation-disabled', 'Federation disabled', { method: 'federationSearchUsers' }); + } + + const federatedUsers = this.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/peerClient.js b/app/federation/server/peerClient.js index 04815509aa36..6639d2d3a3b7 100644 --- a/app/federation/server/peerClient.js +++ b/app/federation/server/peerClient.js @@ -4,14 +4,12 @@ import { callbacks } from '../../callbacks'; import { settings } from '../../settings'; import { FederationEvents, FederationKeys, Messages, Rooms, Subscriptions, Users } from '../../models'; -import { Federation } from '.'; -import peerDNS from './peerDNS'; -import peerHTTP from './peerHTTP'; import { updateStatus } from './settingsUpdater'; import { logger } from './logger'; import { FederatedMessage, FederatedRoom, FederatedUser } from './federatedResources'; +import { Federation } from './'; -class PeerClient { +export class PeerClient { constructor() { this.config = {}; @@ -69,7 +67,7 @@ class PeerClient { if (this.config.hub.active) { updateStatus('Registering with Hub...'); - return peerDNS.register(this.peer); + return Federation.peerDNS.register(this.peer); } return true; @@ -143,7 +141,7 @@ class PeerClient { const { peer: domain } = e; - const peer = peerDNS.searchPeer(domain); + const peer = Federation.peerDNS.searchPeer(domain); if (!peer || !peer.public_key) { this.log(`Could not find valid peer:${ domain }`); @@ -159,7 +157,7 @@ class PeerClient { // Encrypt with the local private key payload = Federation.privateKey.encryptPrivate(payload); - peerHTTP.request(peer, 'POST', '/api/v1/federation.events', { payload }, { total: 5, stepSize: 500, stepMultiplier: 10 }); + Federation.peerHTTP.request(peer, 'POST', '/api/v1/federation.events', { payload }, { total: 5, stepSize: 500, stepMultiplier: 10 }); FederationEvents.setEventAsFullfilled(e); } catch (err) { @@ -242,22 +240,22 @@ class PeerClient { // Users // // ##### - findUsers(email, options = {}) { - const [username, domain] = email.split('@'); + findUsers(identifier, options = {}) { + const [username, domain] = identifier.split('@'); const { peer: { domain: localPeerDomain } } = this; let peer = null; try { - peer = peerDNS.searchPeer(options.domainOverride || domain); + 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 } } = peerHTTP.request(peer, 'GET', `/api/v1/federation.users?${ qs.stringify({ username, domain, emailOnly: options.emailOnly }) }`); + const { data: { federatedUsers: remoteFederatedUsers } } = Federation.peerHTTP.request(peer, 'GET', `/api/v1/federation.users?${ qs.stringify({ username, domain, usernameOnly: options.usernameOnly }) }`); const federatedUsers = []; @@ -268,7 +266,7 @@ class PeerClient { 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:${ email } at ${ peer.domain }`); + throw new Meteor.Error('federation-user-does-not-exist', `Could not find user:${ identifier } at ${ peer.domain }`); } } @@ -283,13 +281,13 @@ class PeerClient { let peer = null; try { - peer = peerDNS.searchPeer(domain); + 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 } } = peerHTTP.request(peer, 'GET', `/api/v1/federation.uploads?${ qs.stringify({ upload_id: fileId }) }`); + const { data: { upload, buffer } } = Federation.peerHTTP.request(peer, 'GET', `/api/v1/federation.uploads?${ qs.stringify({ upload_id: fileId }) }`); return { upload, buffer: Buffer.from(buffer) }; } @@ -611,5 +609,3 @@ class PeerClient { FederationEvents.userUnmuted(federatedRoom, federatedUnmutedUser, federatedUserWhoUnmuted, { skipPeers: [localPeerDomain] }); } } - -export default new PeerClient(); diff --git a/app/federation/server/peerDNS.js b/app/federation/server/peerDNS.js index 75e999703bee..1959cfa6ab11 100644 --- a/app/federation/server/peerDNS.js +++ b/app/federation/server/peerDNS.js @@ -3,13 +3,13 @@ import { Meteor } from 'meteor/meteor'; import { FederationDNSCache } from '../../models'; import { logger } from './logger'; -import peerHTTP from './peerHTTP'; import { updateStatus } from './settingsUpdater'; +import { Federation } from './'; const dnsResolveSRV = Meteor.wrapAsync(dns.resolveSrv); const dnsResolveTXT = Meteor.wrapAsync(dns.resolveTxt); -class PeerDNS { +export class PeerDNS { constructor() { this.config = {}; } @@ -44,7 +44,7 @@ class PeerDNS { // Attempt to register peer try { - peerHTTP.request(this.HubPeer, 'POST', '/api/v1/peers', { uniqueId, domain, url, public_key }, { total: 5, stepSize: 1000, tryToUpdateDNS: false }, headers); + 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!'); @@ -114,7 +114,7 @@ class PeerDNS { this.log(`getPeerUsingHub: ${ domain }`); // If there is no DNS entry for that, get from the Hub - const { data: { peer } } = peerHTTP.simpleRequest(this.HubPeer, 'GET', `/api/v1/peers?search=${ domain }`); + const { data: { peer } } = Federation.peerHTTP.simpleRequest(this.HubPeer, 'GET', `/api/v1/peers?search=${ domain }`); return peer; } @@ -169,5 +169,3 @@ class PeerDNS { } } } - -export default new PeerDNS(); diff --git a/app/federation/server/peerHTTP.js b/app/federation/server/peerHTTP.js deleted file mode 100644 index f96967ea4890..000000000000 --- a/app/federation/server/peerHTTP.js +++ /dev/null @@ -1,126 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { HTTP } from 'meteor/http'; - -import { logger } from './logger'; -import peerDNS from './peerDNS'; - -// Should skip the retry if the error is one of the below? -const errorsToSkipRetrying = [ - 'error-app-prevented-sending', -]; - -function skipRetryOnSpecificError(err) { - return errorsToSkipRetrying.includes(err && err.errorType); -} - -// Delay method to wait a little bit before retrying -const delay = Meteor.wrapAsync(function(ms, callback) { - Meteor.setTimeout(function() { - callback(null); - }, ms); -}); - -function doSimpleRequest(peer, method, uri, body, headers = {}) { - this.log(`Request: ${ method } ${ uri }`); - - const { url: serverBaseURL } = peer; - - const url = `${ serverBaseURL }${ uri }`; - - let data = null; - - if (method === 'POST' || method === 'PUT') { - data = body; - } - - this.log(`Sending request: ${ method } - ${ uri }`); - - return HTTP.call(method, url, { data, timeout: 2000, headers: { ...headers, 'x-federation-domain': this.config.peer.domain } }); -} - -// -// Actually does the request, handling retries and everything -function doRequest(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 doSimpleRequest.call(this, 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 = 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 - if (skipRetryOnSpecificError(err && err.response && err.response.data)) { - this.log('Retry: skipping due to specific error'); - - 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); - } - } -} - -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) { - return doSimpleRequest.call(this, peer, method, uri, body, headers); - } - - // - // Request trying to find DNS entries - request(peer, method, uri, body, retryInfo = {}, headers = {}) { - return doRequest.call(this, peer, method, uri, body, retryInfo, headers); - } -} - -export default new PeerHTTP(); diff --git a/app/federation/server/peerServer/index.js b/app/federation/server/peerServer/index.js index d8144d71ed15..e1da97c3327a 100644 --- a/app/federation/server/peerServer/index.js +++ b/app/federation/server/peerServer/index.js @@ -1,8 +1,6 @@ -import peerServer from './peerServer'; - // Setup routes import './routes/events'; import './routes/uploads'; import './routes/users'; -export default peerServer; +export { PeerServer } from './PeerServer'; diff --git a/app/federation/server/peerServer/peerServer.js b/app/federation/server/peerServer/peerServer.js index 643017b1e17c..6f7e8f84f1ba 100644 --- a/app/federation/server/peerServer/peerServer.js +++ b/app/federation/server/peerServer/peerServer.js @@ -5,9 +5,9 @@ import { Rooms, Subscriptions } from '../../../models'; import { FederatedMessage, FederatedRoom, FederatedUser } from '../federatedResources'; import { logger } from '../logger.js'; -import peerClient from '../peerClient'; +import { Federation } from '../'; -class PeerServer { +export class PeerServer { constructor() { this.config = {}; this.enabled = false; @@ -75,7 +75,7 @@ class PeerServer { federatedRoom.createUsers(); // Then, create the room, if needed - federatedRoom.create(); + federatedRoom.create(true); } handleUserJoinedEvent(e) { @@ -93,7 +93,7 @@ class PeerServer { const localUser = federatedUser.create(); // Callback management - peerClient.addCallbackToSkip('afterAddedToRoom', federatedUser.getFederationId()); + Federation.peerClient.addCallbackToSkip('afterAddedToRoom', federatedUser.getFederationId()); // Add the user to the room addUserToRoom(federatedRoom.room._id, localUser, null, false); @@ -129,7 +129,7 @@ class PeerServer { const localUser = federatedUser.create(); // Callback management - peerClient.addCallbackToSkip('afterAddedToRoom', federatedUser.getFederationId()); + Federation.peerClient.addCallbackToSkip('afterAddedToRoom', federatedUser.getFederationId()); // Add the user to the room addUserToRoom(federatedRoom.room._id, localUser, localInviter, false); @@ -156,7 +156,7 @@ class PeerServer { const localUser = federatedUser.getLocalUser(); // Callback management - peerClient.addCallbackToSkip('beforeLeaveRoom', federatedUser.getFederationId()); + Federation.peerClient.addCallbackToSkip('beforeLeaveRoom', federatedUser.getFederationId()); // Remove the user from the room removeUserFromRoom(federatedRoom.room._id, localUser); @@ -187,7 +187,7 @@ class PeerServer { const localUserWhoRemoved = federatedUserWhoRemoved.getLocalUser(); // Callback management - peerClient.addCallbackToSkip('beforeRemoveFromRoom', federatedUser.getFederationId()); + Federation.peerClient.addCallbackToSkip('beforeRemoveFromRoom', federatedUser.getFederationId()); // Remove the user from the room removeUserFromRoom(federatedRoom.room._id, localUser, { byUser: localUserWhoRemoved }); @@ -260,7 +260,7 @@ class PeerServer { const federatedMessage = new FederatedMessage(localPeerDomain, message); // Callback management - peerClient.addCallbackToSkip('afterSaveMessage', federatedMessage.getFederationId()); + Federation.peerClient.addCallbackToSkip('afterSaveMessage', federatedMessage.getFederationId()); // Create the federated message federatedMessage.create(); @@ -280,7 +280,7 @@ class PeerServer { const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id); // Callback management - peerClient.addCallbackToSkip('afterSaveMessage', federatedMessage.getFederationId()); + Federation.peerClient.addCallbackToSkip('afterSaveMessage', federatedMessage.getFederationId()); // Update the federated message federatedMessage.update(federatedUser); @@ -302,7 +302,7 @@ class PeerServer { const localAuthor = federatedMessage.federatedAuthor.getLocalUser(); // Callback management - peerClient.addCallbackToSkip('afterDeleteMessage', federatedMessage.getFederationId()); + Federation.peerClient.addCallbackToSkip('afterDeleteMessage', federatedMessage.getFederationId()); // Create the federated message deleteMessage(localMessage, localAuthor); @@ -318,7 +318,7 @@ class PeerServer { // Load the federated room const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id); - peerClient.addCallbackToSkip('afterReadMessages', federatedRoom.getFederationId()); + Federation.peerClient.addCallbackToSkip('afterReadMessages', federatedRoom.getFederationId()); // Load the user who left const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id); @@ -352,7 +352,7 @@ class PeerServer { const localMessage = federatedMessage.getLocalMessage(); // Callback management - peerClient.addCallbackToSkip('afterSetReaction', federatedMessage.getFederationId()); + Federation.peerClient.addCallbackToSkip('afterSetReaction', federatedMessage.getFederationId()); // Set message reaction setReaction(localRoom, localUser, localMessage, reaction, shouldReact); @@ -378,11 +378,9 @@ class PeerServer { const localMessage = federatedMessage.getLocalMessage(); // Callback management - peerClient.addCallbackToSkip('afterUnsetReaction', federatedMessage.getFederationId()); + Federation.peerClient.addCallbackToSkip('afterUnsetReaction', federatedMessage.getFederationId()); // Unset message reaction setReaction(localRoom, localUser, localMessage, reaction, shouldReact); } } - -export default new PeerServer(); diff --git a/app/federation/server/peerServer/routes/events.js b/app/federation/server/peerServer/routes/events.js index 1a75fda1d9a0..f8703a6d0dec 100644 --- a/app/federation/server/peerServer/routes/events.js +++ b/app/federation/server/peerServer/routes/events.js @@ -3,12 +3,9 @@ import { FederationKeys } from '../../../../models'; import { Federation } from '../..'; -import peerDNS from '../../peerDNS'; -import peerServer from '../peerServer'; - API.v1.addRoute('federation.events', { authRequired: false }, { post() { - if (!peerServer.enabled) { + if (!Federation.peerServer.enabled) { return API.v1.failure('Not found'); } @@ -22,7 +19,7 @@ API.v1.addRoute('federation.events', { authRequired: false }, { const remotePeerDomain = this.request.headers['x-federation-domain']; - const peer = peerDNS.searchPeer(remotePeerDomain); + const peer = Federation.peerDNS.searchPeer(remotePeerDomain); if (!peer) { return API.v1.failure('Could not find valid peer'); @@ -43,62 +40,62 @@ API.v1.addRoute('federation.events', { authRequired: false }, { return API.v1.failure('Event was not sent'); } - peerServer.log(`Received event:${ e.t }`); + Federation.peerServer.log(`Received event:${ e.t }`); try { switch (e.t) { case 'drc': - peerServer.handleDirectRoomCreatedEvent(e); + Federation.peerServer.handleDirectRoomCreatedEvent(e); break; case 'roc': - peerServer.handleRoomCreatedEvent(e); + Federation.peerServer.handleRoomCreatedEvent(e); break; case 'usj': - peerServer.handleUserJoinedEvent(e); + Federation.peerServer.handleUserJoinedEvent(e); break; case 'usa': - peerServer.handleUserAddedEvent(e); + Federation.peerServer.handleUserAddedEvent(e); break; case 'usl': - peerServer.handleUserLeftEvent(e); + Federation.peerServer.handleUserLeftEvent(e); break; case 'usr': - peerServer.handleUserRemovedEvent(e); + Federation.peerServer.handleUserRemovedEvent(e); break; case 'usm': - peerServer.handleUserMutedEvent(e); + Federation.peerServer.handleUserMutedEvent(e); break; case 'usu': - peerServer.handleUserUnmutedEvent(e); + Federation.peerServer.handleUserUnmutedEvent(e); break; case 'msc': - peerServer.handleMessageCreatedEvent(e); + Federation.peerServer.handleMessageCreatedEvent(e); break; case 'msu': - peerServer.handleMessageUpdatedEvent(e); + Federation.peerServer.handleMessageUpdatedEvent(e); break; case 'msd': - peerServer.handleMessageDeletedEvent(e); + Federation.peerServer.handleMessageDeletedEvent(e); break; case 'msr': - peerServer.handleMessagesReadEvent(e); + Federation.peerServer.handleMessagesReadEvent(e); break; case 'mrs': - peerServer.handleMessagesSetReactionEvent(e); + Federation.peerServer.handleMessagesSetReactionEvent(e); break; case 'mru': - peerServer.handleMessagesUnsetReactionEvent(e); + Federation.peerServer.handleMessagesUnsetReactionEvent(e); break; default: throw new Error(`Invalid event:${ e.t }`); } - peerServer.log('Success, responding...'); + Federation.peerServer.log('Success, responding...'); // Respond return API.v1.success(); } catch (err) { - peerServer.log(`Error handling event:${ e.t } - ${ err.toString() }`); + Federation.peerServer.log(`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/uploads.js b/app/federation/server/peerServer/routes/uploads.js index 5efd8e2c368a..696a10096529 100644 --- a/app/federation/server/peerServer/routes/uploads.js +++ b/app/federation/server/peerServer/routes/uploads.js @@ -3,11 +3,11 @@ import { API } from '../../../../api'; import { Uploads } from '../../../../models'; import { FileUpload } from '../../../../file-upload'; -import peerServer from '../peerServer'; +import { Federation } from '../../'; API.v1.addRoute('federation.uploads', { authRequired: false }, { get() { - if (!peerServer.enabled) { + if (!Federation.peerServer.enabled) { return API.v1.failure('Not found'); } diff --git a/app/federation/server/peerServer/routes/users.js b/app/federation/server/peerServer/routes/users.js index d20cee692d03..4e95e5fd5f33 100644 --- a/app/federation/server/peerServer/routes/users.js +++ b/app/federation/server/peerServer/routes/users.js @@ -2,28 +2,28 @@ import { API } from '../../../../api'; import { Users } from '../../../../models'; import { FederatedUser } from '../../federatedResources'; -import peerServer from '../peerServer'; +import { Federation } from '../../'; API.v1.addRoute('federation.users', { authRequired: false }, { get() { - if (!peerServer.enabled) { + if (!Federation.peerServer.enabled) { return API.v1.failure('Not found'); } - const { peer: { domain: localPeerDomain } } = peerServer.config; + const { peer: { domain: localPeerDomain } } = Federation.peerServer.config; - const { username, domain, emailOnly } = this.requestParams(); + const { username, domain, usernameOnly } = this.requestParams(); const email = `${ username }@${ domain }`; - peerServer.log(`[users] Trying to find user by username:${ username } and email:${ email }`); + Federation.peerServer.log(`[users] Trying to find user by username:${ username } and email:${ email }`); const query = { type: 'user', }; - if (emailOnly === 'true') { - query['emails.address'] = email; + if (usernameOnly === 'true') { + query.username = username; } else { query.$or = [ { name: username }, From e6250123677619b82ef45f5c7570a8efdf400cd1 Mon Sep 17 00:00:00 2001 From: Alan Sikora Date: Thu, 28 Mar 2019 14:20:02 -0300 Subject: [PATCH 2/8] git fix --- app/federation/README.md | 1 - app/federation/client/index.js | 23 - app/federation/server/PeerClient.js | 611 ------------------ app/federation/server/PeerDNS.js | 171 ----- app/federation/server/PeerHTTP/PeerHTTP.js | 100 --- app/federation/server/PeerHTTP/index.js | 1 - app/federation/server/PeerHTTP/utils.js | 19 - .../server/PeerServer/PeerServer.js | 386 ----------- app/federation/server/PeerServer/index.js | 6 - .../server/PeerServer/routes/events.js | 103 --- .../server/PeerServer/routes/uploads.js | 28 - .../server/PeerServer/routes/users.js | 49 -- .../federatedResources/FederatedMessage.js | 263 -------- .../federatedResources/FederatedResource.js | 17 - .../federatedResources/FederatedRoom.js | 268 -------- .../federatedResources/FederatedUser.js | 122 ---- .../server/federatedResources/index.js | 4 - app/federation/server/federation-settings.js | 68 -- app/federation/server/index.js | 174 ----- app/federation/server/logger.js | 12 - app/federation/server/methods/addUser.js | 44 -- app/federation/server/methods/searchUsers.js | 19 - app/federation/server/peerClient.js | 611 ------------------ app/federation/server/peerDNS.js | 171 ----- app/federation/server/peerServer/index.js | 6 - .../server/peerServer/peerServer.js | 386 ----------- .../server/peerServer/routes/events.js | 103 --- .../server/peerServer/routes/uploads.js | 28 - .../server/peerServer/routes/users.js | 49 -- app/federation/server/settingsUpdater.js | 17 - 30 files changed, 3860 deletions(-) delete mode 100644 app/federation/README.md delete mode 100644 app/federation/client/index.js delete mode 100644 app/federation/server/PeerClient.js delete mode 100644 app/federation/server/PeerDNS.js delete mode 100644 app/federation/server/PeerHTTP/PeerHTTP.js delete mode 100644 app/federation/server/PeerHTTP/index.js delete mode 100644 app/federation/server/PeerHTTP/utils.js delete mode 100644 app/federation/server/PeerServer/PeerServer.js delete mode 100644 app/federation/server/PeerServer/index.js delete mode 100644 app/federation/server/PeerServer/routes/events.js delete mode 100644 app/federation/server/PeerServer/routes/uploads.js delete mode 100644 app/federation/server/PeerServer/routes/users.js delete mode 100644 app/federation/server/federatedResources/FederatedMessage.js delete mode 100644 app/federation/server/federatedResources/FederatedResource.js delete mode 100644 app/federation/server/federatedResources/FederatedRoom.js delete mode 100644 app/federation/server/federatedResources/FederatedUser.js delete mode 100644 app/federation/server/federatedResources/index.js delete mode 100644 app/federation/server/federation-settings.js delete mode 100644 app/federation/server/index.js delete mode 100644 app/federation/server/logger.js delete mode 100644 app/federation/server/methods/addUser.js delete mode 100644 app/federation/server/methods/searchUsers.js delete mode 100644 app/federation/server/peerClient.js delete mode 100644 app/federation/server/peerDNS.js delete mode 100644 app/federation/server/peerServer/index.js delete mode 100644 app/federation/server/peerServer/peerServer.js delete mode 100644 app/federation/server/peerServer/routes/events.js delete mode 100644 app/federation/server/peerServer/routes/uploads.js delete mode 100644 app/federation/server/peerServer/routes/users.js delete mode 100644 app/federation/server/settingsUpdater.js diff --git a/app/federation/README.md b/app/federation/README.md deleted file mode 100644 index e8f08c102332..000000000000 --- a/app/federation/README.md +++ /dev/null @@ -1 +0,0 @@ -##Rocket.Chat Federation \ No newline at end of file diff --git a/app/federation/client/index.js b/app/federation/client/index.js deleted file mode 100644 index 573845cba35a..000000000000 --- a/app/federation/client/index.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 6639d2d3a3b7..000000000000 --- a/app/federation/server/PeerClient.js +++ /dev/null @@ -1,611 +0,0 @@ -import qs from 'querystring'; -import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../callbacks'; -import { settings } from '../../settings'; -import { FederationEvents, FederationKeys, Messages, Rooms, Subscriptions, Users } from '../../models'; - -import { updateStatus } from './settingsUpdater'; -import { logger } from './logger'; -import { FederatedMessage, FederatedRoom, FederatedUser } from './federatedResources'; -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-join-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-join-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-leave-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 } = 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 }, { 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(); - - for (const e of events) { - 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] }); - } - - 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] }); - } - - 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 is federated - if (!FederatedRoom.isFederated(localPeerDomain, room, { checkUsingUsers: true })) { 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] }); - } - } - - 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(); - } - - 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(); - } - - 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] }); - } - } - - 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] }); - } - - 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] }); - } - - 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] }); - } - - 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] }); - } - - 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] }); - } - - 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] }); - } -} diff --git a/app/federation/server/PeerDNS.js b/app/federation/server/PeerDNS.js deleted file mode 100644 index 1959cfa6ab11..000000000000 --- a/app/federation/server/PeerDNS.js +++ /dev/null @@ -1,171 +0,0 @@ -import dns from 'dns'; -import { Meteor } from 'meteor/meteor'; -import { FederationDNSCache } from '../../models'; - -import { logger } from './logger'; -import { updateStatus } from './settingsUpdater'; -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) { - this.updatePeerDNS(domain); - - peer = FederationDNSCache.findOneByDomain(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 public key from the TXT record - const txtRecords = dnsResolveTXT(`rocketchat-public-key.${ domain }`); - - // Get the first TXT record, this subdomain should have only a single record - const txtRecord = txtRecords[0]; - - // If there is no record, skip - if (!txtRecord) { - throw new Meteor.Error('ENOTFOUND', 'Could not find public key entry on TXT records'); - } - - const publicKey = txtRecord.join(''); - - const protocol = srvEntry.name === 'localhost' ? 'http' : 'https'; - - 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; - - 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 }`); - } - - peer = this.getPeerUsingHub(domain); - } - - 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 f45e59eb0706..000000000000 --- a/app/federation/server/PeerHTTP/PeerHTTP.js +++ /dev/null @@ -1,100 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { HTTP } from 'meteor/http'; - -import { logger } from '../logger'; -import { Federation } from '../'; - -import { skipRetryOnSpecificError, delay } from './utils'; - -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) { - this.log(`Request: ${ method } ${ uri }`); - - const { url: serverBaseURL } = peer; - - const url = `${ serverBaseURL }${ uri }`; - - let data = null; - - if (method === 'POST' || method === 'PUT') { - data = body; - } - - this.log(`Sending request: ${ method } - ${ uri }`); - - 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 - if (skipRetryOnSpecificError(err)) { - this.log('Retry: skipping due to specific error'); - - 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 3c9e957f1cc9..000000000000 --- 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 a3d5d153d612..000000000000 --- a/app/federation/server/PeerHTTP/utils.js +++ /dev/null @@ -1,19 +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', -]; - -export function skipRetryOnSpecificError(err) { - err = err && err.response && err.response.data; - return errorsToSkipRetrying.includes(err && err.errorType); -} - -// 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/PeerServer/PeerServer.js b/app/federation/server/PeerServer/PeerServer.js deleted file mode 100644 index 6f7e8f84f1ba..000000000000 --- a/app/federation/server/PeerServer/PeerServer.js +++ /dev/null @@ -1,386 +0,0 @@ -import { callbacks } from '../../../callbacks'; -import { setReaction } from '../../../reactions/server'; -import { addUserToRoom, removeUserFromRoom, deleteMessage } from '../../../lib'; -import { Rooms, Subscriptions } 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(); - } - - 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); - } - - 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(); - } - - 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(); - } - - 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(); - } - - 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(); - } - - 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 e1da97c3327a..000000000000 --- 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 f8703a6d0dec..000000000000 --- a/app/federation/server/PeerServer/routes/events.js +++ /dev/null @@ -1,103 +0,0 @@ -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); - - // Decrypt with the peer's public key - let payload = FederationKeys.loadKey(peer.public_key, 'public').decryptPublic(payloadBuffer); - - // Decrypt with the local private key - payload = Federation.privateKey.decrypt(payload); - - // 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 '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) { - Federation.peerServer.log(`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/uploads.js b/app/federation/server/PeerServer/routes/uploads.js deleted file mode 100644 index 696a10096529..000000000000 --- a/app/federation/server/PeerServer/routes/uploads.js +++ /dev/null @@ -1,28 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { API } from '../../../../api'; -import { Uploads } from '../../../../models'; -import { FileUpload } from '../../../../file-upload'; - -import { Federation } from '../../'; - -API.v1.addRoute('federation.uploads', { authRequired: false }, { - get() { - if (!Federation.peerServer.enabled) { - return API.v1.failure('Not found'); - } - - const { upload_id } = this.requestParams(); - - const upload = Uploads.findOneById(upload_id); - - if (!upload) { - return API.v1.failure('There is no such file in this server'); - } - - const getFileBuffer = Meteor.wrapAsync(FileUpload.getBuffer, FileUpload); - - const buffer = getFileBuffer(upload); - - return API.v1.success({ upload, buffer }); - }, -}); diff --git a/app/federation/server/PeerServer/routes/users.js b/app/federation/server/PeerServer/routes/users.js deleted file mode 100644 index 4e95e5fd5f33..000000000000 --- a/app/federation/server/PeerServer/routes/users.js +++ /dev/null @@ -1,49 +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/federatedResources/FederatedMessage.js b/app/federation/server/federatedResources/FederatedMessage.js deleted file mode 100644 index 97cb4c273c19..000000000000 --- 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 7ecdb9ec1cd3..000000000000 --- 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 06e5a0a3a855..000000000000 --- a/app/federation/server/federatedResources/FederatedRoom.js +++ /dev/null @@ -1,268 +0,0 @@ -import { createRoom } from '../../../lib'; -import { Rooms, Subscriptions, Users } from '../../../models'; - -import { FederatedResource } from './FederatedResource'; -import { FederatedUser } from './FederatedUser'; - -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) { - if (!owner && room.federation) { - this.federatedOwner = FederatedUser.loadByFederationId(localPeerIdentifier, room.federation.ownerId); - } else { - this.federatedOwner = FederatedUser.loadOrCreate(localPeerIdentifier, owner); - } - } - - // Set base federation - room.federation = room.federation || { - _id: room._id, - peer: localPeerIdentifier, - ownerId: this.federatedOwner ? this.federatedOwner.getFederationId() : null, - }; - - // 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); - - // 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 74f40bbf3168..000000000000 --- a/app/federation/server/federatedResources/FederatedUser.js +++ /dev/null @@ -1,122 +0,0 @@ -import { Users } from '../../../models'; - -import { FederatedResource } from './FederatedResource'; - -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]; - } - - 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 90d98b351cab..000000000000 --- 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 5e507774b282..000000000000 --- a/app/federation/server/federation-settings.js +++ /dev/null @@ -1,68 +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(); - - 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', - }); - - 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', 'https://hub.rocket.chat', { - 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, - }); - - }); -}); diff --git a/app/federation/server/index.js b/app/federation/server/index.js deleted file mode 100644 index 67afc5ce3081..000000000000 --- a/app/federation/server/index.js +++ /dev/null @@ -1,174 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { _ } from 'meteor/underscore'; -import { settings } from '../../settings'; -import { FederationKeys } from '../../models'; -import { getWorkspaceAccessToken } from '../../cloud/server'; - -import './federation-settings'; - -import { logger } from './logger'; -import { PeerClient } from './PeerClient'; -import { PeerDNS } from './PeerDNS'; -import { PeerHTTP } from './PeerHTTP'; -import { PeerServer } from './PeerServer'; -import * as SettingsUpdater from './settingsUpdater'; - -import { addUser } from './methods/addUser'; -import { searchUsers } from './methods/searchUsers'; - -const peerClient = new PeerClient(); -const peerDNS = new PeerDNS(); -const peerHTTP = new PeerHTTP(); -const peerServer = new PeerServer(); - -export const Federation = { - enabled: false, - privateKey: null, - publicKey: null, - usingHub: null, - uniqueId: null, - localIdentifier: null, - - peerClient, - peerDNS, - peerHTTP, - peerServer, -}; - -// Add Federation methods with bound context -Federation.methods = { - addUser: addUser.bind(Federation), - searchUsers: searchUsers.bind(Federation), -}; - -// 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(); - -const updateSettings = _.debounce(Meteor.bindEnvironment(function() { - const _enabled = settings.get('FEDERATION_Enabled'); - - if (!_enabled) { return; } - - // 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; - } - - logger.setup.info('Updating settings...'); - - // Normalize the config values - const config = { - hub: { - active: _discoveryMethod === 'hub', - url: _hubUrl.replace(/\/+$/, ''), - }, - peer: { - uniqueId: _uniqueId, - domain: _domain.replace('@', '').trim(), - url: _peerUrl.replace(/\/+$/, ''), - public_key: FederationKeys.getPublicKeyString(), - }, - cloud: { - token: getWorkspaceAccessToken(), - }, - }; - - // 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); diff --git a/app/federation/server/logger.js b/app/federation/server/logger.js deleted file mode 100644 index 36e915c2813f..000000000000 --- a/app/federation/server/logger.js +++ /dev/null @@ -1,12 +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', - }, -}); diff --git a/app/federation/server/methods/addUser.js b/app/federation/server/methods/addUser.js deleted file mode 100644 index bd87b5ee6147..000000000000 --- a/app/federation/server/methods/addUser.js +++ /dev/null @@ -1,44 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Users } from '../../../models'; - -import { logger } from '../logger'; - -export function addUser(identifier) { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'Federation.addUser' }); - } - - if (!this.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] = this.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); - } 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/searchUsers.js b/app/federation/server/methods/searchUsers.js deleted file mode 100644 index 5dade6646f75..000000000000 --- a/app/federation/server/methods/searchUsers.js +++ /dev/null @@ -1,19 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -export function searchUsers(identifier) { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'federationSearchUsers' }); - } - - if (!this.peerClient.enabled) { - throw new Meteor.Error('error-federation-disabled', 'Federation disabled', { method: 'federationSearchUsers' }); - } - - const federatedUsers = this.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/peerClient.js b/app/federation/server/peerClient.js deleted file mode 100644 index 6639d2d3a3b7..000000000000 --- a/app/federation/server/peerClient.js +++ /dev/null @@ -1,611 +0,0 @@ -import qs from 'querystring'; -import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../callbacks'; -import { settings } from '../../settings'; -import { FederationEvents, FederationKeys, Messages, Rooms, Subscriptions, Users } from '../../models'; - -import { updateStatus } from './settingsUpdater'; -import { logger } from './logger'; -import { FederatedMessage, FederatedRoom, FederatedUser } from './federatedResources'; -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-join-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-join-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-leave-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 } = 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 }, { 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(); - - for (const e of events) { - 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] }); - } - - 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] }); - } - - 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 is federated - if (!FederatedRoom.isFederated(localPeerDomain, room, { checkUsingUsers: true })) { 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] }); - } - } - - 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(); - } - - 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(); - } - - 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] }); - } - } - - 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] }); - } - - 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] }); - } - - 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] }); - } - - 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] }); - } - - 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] }); - } - - 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] }); - } -} diff --git a/app/federation/server/peerDNS.js b/app/federation/server/peerDNS.js deleted file mode 100644 index 1959cfa6ab11..000000000000 --- a/app/federation/server/peerDNS.js +++ /dev/null @@ -1,171 +0,0 @@ -import dns from 'dns'; -import { Meteor } from 'meteor/meteor'; -import { FederationDNSCache } from '../../models'; - -import { logger } from './logger'; -import { updateStatus } from './settingsUpdater'; -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) { - this.updatePeerDNS(domain); - - peer = FederationDNSCache.findOneByDomain(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 public key from the TXT record - const txtRecords = dnsResolveTXT(`rocketchat-public-key.${ domain }`); - - // Get the first TXT record, this subdomain should have only a single record - const txtRecord = txtRecords[0]; - - // If there is no record, skip - if (!txtRecord) { - throw new Meteor.Error('ENOTFOUND', 'Could not find public key entry on TXT records'); - } - - const publicKey = txtRecord.join(''); - - const protocol = srvEntry.name === 'localhost' ? 'http' : 'https'; - - 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; - - 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 }`); - } - - peer = this.getPeerUsingHub(domain); - } - - 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/peerServer/index.js b/app/federation/server/peerServer/index.js deleted file mode 100644 index e1da97c3327a..000000000000 --- 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/peerServer.js b/app/federation/server/peerServer/peerServer.js deleted file mode 100644 index 6f7e8f84f1ba..000000000000 --- a/app/federation/server/peerServer/peerServer.js +++ /dev/null @@ -1,386 +0,0 @@ -import { callbacks } from '../../../callbacks'; -import { setReaction } from '../../../reactions/server'; -import { addUserToRoom, removeUserFromRoom, deleteMessage } from '../../../lib'; -import { Rooms, Subscriptions } 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(); - } - - 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); - } - - 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(); - } - - 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(); - } - - 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(); - } - - 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(); - } - - 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/routes/events.js b/app/federation/server/peerServer/routes/events.js deleted file mode 100644 index f8703a6d0dec..000000000000 --- a/app/federation/server/peerServer/routes/events.js +++ /dev/null @@ -1,103 +0,0 @@ -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); - - // Decrypt with the peer's public key - let payload = FederationKeys.loadKey(peer.public_key, 'public').decryptPublic(payloadBuffer); - - // Decrypt with the local private key - payload = Federation.privateKey.decrypt(payload); - - // 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 '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) { - Federation.peerServer.log(`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/uploads.js b/app/federation/server/peerServer/routes/uploads.js deleted file mode 100644 index 696a10096529..000000000000 --- a/app/federation/server/peerServer/routes/uploads.js +++ /dev/null @@ -1,28 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { API } from '../../../../api'; -import { Uploads } from '../../../../models'; -import { FileUpload } from '../../../../file-upload'; - -import { Federation } from '../../'; - -API.v1.addRoute('federation.uploads', { authRequired: false }, { - get() { - if (!Federation.peerServer.enabled) { - return API.v1.failure('Not found'); - } - - const { upload_id } = this.requestParams(); - - const upload = Uploads.findOneById(upload_id); - - if (!upload) { - return API.v1.failure('There is no such file in this server'); - } - - const getFileBuffer = Meteor.wrapAsync(FileUpload.getBuffer, FileUpload); - - const buffer = getFileBuffer(upload); - - return API.v1.success({ upload, buffer }); - }, -}); diff --git a/app/federation/server/peerServer/routes/users.js b/app/federation/server/peerServer/routes/users.js deleted file mode 100644 index 4e95e5fd5f33..000000000000 --- a/app/federation/server/peerServer/routes/users.js +++ /dev/null @@ -1,49 +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/settingsUpdater.js b/app/federation/server/settingsUpdater.js deleted file mode 100644 index 344d6545e954..000000000000 --- 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); -} From 4dd3bcb5acd0b9b33ee5c50f6793e5b268494ee9 Mon Sep 17 00:00:00 2001 From: Alan Sikora Date: Thu, 28 Mar 2019 14:21:45 -0300 Subject: [PATCH 3/8] Better federation file and code organization --- app/federation/README.md | 1 + app/federation/client/index.js | 23 + app/federation/server/PeerClient | 611 ++++++++++++++++++ app/federation/server/PeerDNS.js | 171 +++++ app/federation/server/PeerHTTP/PeerHTTP.js | 100 +++ app/federation/server/PeerHTTP/index.js | 1 + app/federation/server/PeerHTTP/utils.js | 19 + .../server/PeerServer/PeerServer.js | 386 +++++++++++ app/federation/server/PeerServer/index.js | 6 + .../server/PeerServer/routes/events.js | 103 +++ .../server/PeerServer/routes/uploads.js | 28 + .../server/PeerServer/routes/users.js | 49 ++ .../federatedResources/FederatedMessage.js | 263 ++++++++ .../federatedResources/FederatedResource.js | 17 + .../federatedResources/FederatedRoom.js | 268 ++++++++ .../federatedResources/FederatedUser.js | 122 ++++ .../server/federatedResources/index.js | 4 + app/federation/server/federation-settings.js | 68 ++ app/federation/server/index.js | 174 +++++ app/federation/server/logger.js | 12 + app/federation/server/methods/addUser.js | 44 ++ app/federation/server/methods/searchUsers.js | 19 + app/federation/server/settingsUpdater.js | 17 + 23 files changed, 2506 insertions(+) create mode 100644 app/federation/README.md create mode 100644 app/federation/client/index.js create mode 100644 app/federation/server/PeerClient create mode 100644 app/federation/server/PeerDNS.js create mode 100644 app/federation/server/PeerHTTP/PeerHTTP.js create mode 100644 app/federation/server/PeerHTTP/index.js create mode 100644 app/federation/server/PeerHTTP/utils.js create mode 100644 app/federation/server/PeerServer/PeerServer.js create mode 100644 app/federation/server/PeerServer/index.js create mode 100644 app/federation/server/PeerServer/routes/events.js create mode 100644 app/federation/server/PeerServer/routes/uploads.js create mode 100644 app/federation/server/PeerServer/routes/users.js create mode 100644 app/federation/server/federatedResources/FederatedMessage.js create mode 100644 app/federation/server/federatedResources/FederatedResource.js create mode 100644 app/federation/server/federatedResources/FederatedRoom.js create mode 100644 app/federation/server/federatedResources/FederatedUser.js create mode 100644 app/federation/server/federatedResources/index.js create mode 100644 app/federation/server/federation-settings.js create mode 100644 app/federation/server/index.js create mode 100644 app/federation/server/logger.js create mode 100644 app/federation/server/methods/addUser.js create mode 100644 app/federation/server/methods/searchUsers.js create mode 100644 app/federation/server/settingsUpdater.js diff --git a/app/federation/README.md b/app/federation/README.md new file mode 100644 index 000000000000..e8f08c102332 --- /dev/null +++ b/app/federation/README.md @@ -0,0 +1 @@ +##Rocket.Chat Federation \ No newline at end of file diff --git a/app/federation/client/index.js b/app/federation/client/index.js new file mode 100644 index 000000000000..573845cba35a --- /dev/null +++ b/app/federation/client/index.js @@ -0,0 +1,23 @@ +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 b/app/federation/server/PeerClient new file mode 100644 index 000000000000..6639d2d3a3b7 --- /dev/null +++ b/app/federation/server/PeerClient @@ -0,0 +1,611 @@ +import qs from 'querystring'; +import { Meteor } from 'meteor/meteor'; +import { callbacks } from '../../callbacks'; +import { settings } from '../../settings'; +import { FederationEvents, FederationKeys, Messages, Rooms, Subscriptions, Users } from '../../models'; + +import { updateStatus } from './settingsUpdater'; +import { logger } from './logger'; +import { FederatedMessage, FederatedRoom, FederatedUser } from './federatedResources'; +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-join-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-join-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-leave-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 } = 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 }, { 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(); + + for (const e of events) { + 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] }); + } + + 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] }); + } + + 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 is federated + if (!FederatedRoom.isFederated(localPeerDomain, room, { checkUsingUsers: true })) { 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] }); + } + } + + 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(); + } + + 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(); + } + + 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] }); + } + } + + 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] }); + } + + 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] }); + } + + 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] }); + } + + 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] }); + } + + 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] }); + } + + 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] }); + } +} diff --git a/app/federation/server/PeerDNS.js b/app/federation/server/PeerDNS.js new file mode 100644 index 000000000000..1959cfa6ab11 --- /dev/null +++ b/app/federation/server/PeerDNS.js @@ -0,0 +1,171 @@ +import dns from 'dns'; +import { Meteor } from 'meteor/meteor'; +import { FederationDNSCache } from '../../models'; + +import { logger } from './logger'; +import { updateStatus } from './settingsUpdater'; +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) { + this.updatePeerDNS(domain); + + peer = FederationDNSCache.findOneByDomain(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 public key from the TXT record + const txtRecords = dnsResolveTXT(`rocketchat-public-key.${ domain }`); + + // Get the first TXT record, this subdomain should have only a single record + const txtRecord = txtRecords[0]; + + // If there is no record, skip + if (!txtRecord) { + throw new Meteor.Error('ENOTFOUND', 'Could not find public key entry on TXT records'); + } + + const publicKey = txtRecord.join(''); + + const protocol = srvEntry.name === 'localhost' ? 'http' : 'https'; + + 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; + + 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 }`); + } + + peer = this.getPeerUsingHub(domain); + } + + 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 new file mode 100644 index 000000000000..f45e59eb0706 --- /dev/null +++ b/app/federation/server/PeerHTTP/PeerHTTP.js @@ -0,0 +1,100 @@ +import { Meteor } from 'meteor/meteor'; +import { HTTP } from 'meteor/http'; + +import { logger } from '../logger'; +import { Federation } from '../'; + +import { skipRetryOnSpecificError, delay } from './utils'; + +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) { + this.log(`Request: ${ method } ${ uri }`); + + const { url: serverBaseURL } = peer; + + const url = `${ serverBaseURL }${ uri }`; + + let data = null; + + if (method === 'POST' || method === 'PUT') { + data = body; + } + + this.log(`Sending request: ${ method } - ${ uri }`); + + 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 + if (skipRetryOnSpecificError(err)) { + this.log('Retry: skipping due to specific error'); + + 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 new file mode 100644 index 000000000000..3c9e957f1cc9 --- /dev/null +++ b/app/federation/server/PeerHTTP/index.js @@ -0,0 +1 @@ +export { PeerHTTP } from './PeerHTTP'; diff --git a/app/federation/server/PeerHTTP/utils.js b/app/federation/server/PeerHTTP/utils.js new file mode 100644 index 000000000000..a3d5d153d612 --- /dev/null +++ b/app/federation/server/PeerHTTP/utils.js @@ -0,0 +1,19 @@ +import { Meteor } from 'meteor/meteor'; + +// Should skip the retry if the error is one of the below? +const errorsToSkipRetrying = [ + 'error-app-prevented-sending', +]; + +export function skipRetryOnSpecificError(err) { + err = err && err.response && err.response.data; + return errorsToSkipRetrying.includes(err && err.errorType); +} + +// 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/PeerServer/PeerServer.js b/app/federation/server/PeerServer/PeerServer.js new file mode 100644 index 000000000000..6f7e8f84f1ba --- /dev/null +++ b/app/federation/server/PeerServer/PeerServer.js @@ -0,0 +1,386 @@ +import { callbacks } from '../../../callbacks'; +import { setReaction } from '../../../reactions/server'; +import { addUserToRoom, removeUserFromRoom, deleteMessage } from '../../../lib'; +import { Rooms, Subscriptions } 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(); + } + + 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); + } + + 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(); + } + + 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(); + } + + 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(); + } + + 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(); + } + + 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 new file mode 100644 index 000000000000..e1da97c3327a --- /dev/null +++ b/app/federation/server/PeerServer/index.js @@ -0,0 +1,6 @@ +// 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 new file mode 100644 index 000000000000..f8703a6d0dec --- /dev/null +++ b/app/federation/server/PeerServer/routes/events.js @@ -0,0 +1,103 @@ +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); + + // Decrypt with the peer's public key + let payload = FederationKeys.loadKey(peer.public_key, 'public').decryptPublic(payloadBuffer); + + // Decrypt with the local private key + payload = Federation.privateKey.decrypt(payload); + + // 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 '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) { + Federation.peerServer.log(`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/uploads.js b/app/federation/server/PeerServer/routes/uploads.js new file mode 100644 index 000000000000..696a10096529 --- /dev/null +++ b/app/federation/server/PeerServer/routes/uploads.js @@ -0,0 +1,28 @@ +import { Meteor } from 'meteor/meteor'; +import { API } from '../../../../api'; +import { Uploads } from '../../../../models'; +import { FileUpload } from '../../../../file-upload'; + +import { Federation } from '../../'; + +API.v1.addRoute('federation.uploads', { authRequired: false }, { + get() { + if (!Federation.peerServer.enabled) { + return API.v1.failure('Not found'); + } + + const { upload_id } = this.requestParams(); + + const upload = Uploads.findOneById(upload_id); + + if (!upload) { + return API.v1.failure('There is no such file in this server'); + } + + const getFileBuffer = Meteor.wrapAsync(FileUpload.getBuffer, FileUpload); + + const buffer = getFileBuffer(upload); + + return API.v1.success({ upload, buffer }); + }, +}); diff --git a/app/federation/server/PeerServer/routes/users.js b/app/federation/server/PeerServer/routes/users.js new file mode 100644 index 000000000000..4e95e5fd5f33 --- /dev/null +++ b/app/federation/server/PeerServer/routes/users.js @@ -0,0 +1,49 @@ +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/federatedResources/FederatedMessage.js b/app/federation/server/federatedResources/FederatedMessage.js new file mode 100644 index 000000000000..97cb4c273c19 --- /dev/null +++ b/app/federation/server/federatedResources/FederatedMessage.js @@ -0,0 +1,263 @@ +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 new file mode 100644 index 000000000000..7ecdb9ec1cd3 --- /dev/null +++ b/app/federation/server/federatedResources/FederatedResource.js @@ -0,0 +1,17 @@ +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 new file mode 100644 index 000000000000..06e5a0a3a855 --- /dev/null +++ b/app/federation/server/federatedResources/FederatedRoom.js @@ -0,0 +1,268 @@ +import { createRoom } from '../../../lib'; +import { Rooms, Subscriptions, Users } from '../../../models'; + +import { FederatedResource } from './FederatedResource'; +import { FederatedUser } from './FederatedUser'; + +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) { + if (!owner && room.federation) { + this.federatedOwner = FederatedUser.loadByFederationId(localPeerIdentifier, room.federation.ownerId); + } else { + this.federatedOwner = FederatedUser.loadOrCreate(localPeerIdentifier, owner); + } + } + + // Set base federation + room.federation = room.federation || { + _id: room._id, + peer: localPeerIdentifier, + ownerId: this.federatedOwner ? this.federatedOwner.getFederationId() : null, + }; + + // 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); + + // 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 new file mode 100644 index 000000000000..74f40bbf3168 --- /dev/null +++ b/app/federation/server/federatedResources/FederatedUser.js @@ -0,0 +1,122 @@ +import { Users } from '../../../models'; + +import { FederatedResource } from './FederatedResource'; + +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]; + } + + 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 new file mode 100644 index 000000000000..90d98b351cab --- /dev/null +++ b/app/federation/server/federatedResources/index.js @@ -0,0 +1,4 @@ +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 new file mode 100644 index 000000000000..5e507774b282 --- /dev/null +++ b/app/federation/server/federation-settings.js @@ -0,0 +1,68 @@ +import { Meteor } from 'meteor/meteor'; +import { settings } from '../../settings'; +import { FederationKeys } from '../../models'; + +Meteor.startup(function() { + // const federationUniqueId = FederationKeys.getUniqueId(); + 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', '-', { + 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', + }); + + 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', 'https://hub.rocket.chat', { + 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, + }); + + }); +}); diff --git a/app/federation/server/index.js b/app/federation/server/index.js new file mode 100644 index 000000000000..67afc5ce3081 --- /dev/null +++ b/app/federation/server/index.js @@ -0,0 +1,174 @@ +import { Meteor } from 'meteor/meteor'; +import { _ } from 'meteor/underscore'; +import { settings } from '../../settings'; +import { FederationKeys } from '../../models'; +import { getWorkspaceAccessToken } from '../../cloud/server'; + +import './federation-settings'; + +import { logger } from './logger'; +import { PeerClient } from './PeerClient'; +import { PeerDNS } from './PeerDNS'; +import { PeerHTTP } from './PeerHTTP'; +import { PeerServer } from './PeerServer'; +import * as SettingsUpdater from './settingsUpdater'; + +import { addUser } from './methods/addUser'; +import { searchUsers } from './methods/searchUsers'; + +const peerClient = new PeerClient(); +const peerDNS = new PeerDNS(); +const peerHTTP = new PeerHTTP(); +const peerServer = new PeerServer(); + +export const Federation = { + enabled: false, + privateKey: null, + publicKey: null, + usingHub: null, + uniqueId: null, + localIdentifier: null, + + peerClient, + peerDNS, + peerHTTP, + peerServer, +}; + +// Add Federation methods with bound context +Federation.methods = { + addUser: addUser.bind(Federation), + searchUsers: searchUsers.bind(Federation), +}; + +// 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(); + +const updateSettings = _.debounce(Meteor.bindEnvironment(function() { + const _enabled = settings.get('FEDERATION_Enabled'); + + if (!_enabled) { return; } + + // 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; + } + + logger.setup.info('Updating settings...'); + + // Normalize the config values + const config = { + hub: { + active: _discoveryMethod === 'hub', + url: _hubUrl.replace(/\/+$/, ''), + }, + peer: { + uniqueId: _uniqueId, + domain: _domain.replace('@', '').trim(), + url: _peerUrl.replace(/\/+$/, ''), + public_key: FederationKeys.getPublicKeyString(), + }, + cloud: { + token: getWorkspaceAccessToken(), + }, + }; + + // 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); diff --git a/app/federation/server/logger.js b/app/federation/server/logger.js new file mode 100644 index 000000000000..36e915c2813f --- /dev/null +++ b/app/federation/server/logger.js @@ -0,0 +1,12 @@ +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', + }, +}); diff --git a/app/federation/server/methods/addUser.js b/app/federation/server/methods/addUser.js new file mode 100644 index 000000000000..bd87b5ee6147 --- /dev/null +++ b/app/federation/server/methods/addUser.js @@ -0,0 +1,44 @@ +import { Meteor } from 'meteor/meteor'; +import { Users } from '../../../models'; + +import { logger } from '../logger'; + +export function addUser(identifier) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'Federation.addUser' }); + } + + if (!this.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] = this.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); + } 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/searchUsers.js b/app/federation/server/methods/searchUsers.js new file mode 100644 index 000000000000..5dade6646f75 --- /dev/null +++ b/app/federation/server/methods/searchUsers.js @@ -0,0 +1,19 @@ +import { Meteor } from 'meteor/meteor'; + +export function searchUsers(identifier) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'federationSearchUsers' }); + } + + if (!this.peerClient.enabled) { + throw new Meteor.Error('error-federation-disabled', 'Federation disabled', { method: 'federationSearchUsers' }); + } + + const federatedUsers = this.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/settingsUpdater.js b/app/federation/server/settingsUpdater.js new file mode 100644 index 000000000000..344d6545e954 --- /dev/null +++ b/app/federation/server/settingsUpdater.js @@ -0,0 +1,17 @@ +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); +} From ddaadd97109b1bcf686bf968775b7d9a8969f9b7 Mon Sep 17 00:00:00 2001 From: Alan Sikora Date: Thu, 28 Mar 2019 14:34:14 -0300 Subject: [PATCH 4/8] Rename PeerClient to PeerClient.js --- app/federation/server/{PeerClient => PeerClient.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/federation/server/{PeerClient => PeerClient.js} (100%) diff --git a/app/federation/server/PeerClient b/app/federation/server/PeerClient.js similarity index 100% rename from app/federation/server/PeerClient rename to app/federation/server/PeerClient.js From c9462ea393d50310c067d20ec7563611e11b65ca Mon Sep 17 00:00:00 2001 From: Alan Sikora Date: Thu, 28 Mar 2019 15:37:06 -0300 Subject: [PATCH 5/8] missing file after git case problem --- app/ui/client/views/app/directory.js | 57 +++------------------------- 1 file changed, 6 insertions(+), 51 deletions(-) diff --git a/app/ui/client/views/app/directory.js b/app/ui/client/views/app/directory.js index d2dd074c3d98..9494cc0e5308 100644 --- a/app/ui/client/views/app/directory.js +++ b/app/ui/client/views/app/directory.js @@ -124,27 +124,14 @@ Template.directory.helpers({ let routeConfig; return function(item) { - // This means we need to add this user locally first - if (item.remoteOnly) { - Meteor.call('federationAddUser', item.email, item.domain, (error, federatedUser) => { - if (!federatedUser) { return; } - - // Reload - instance.end.set(false); - // directorySearch.call(instance); - - roomTypes.openRouteLink('d', { name: item.username }); - }); + if (searchType.get() === 'channels') { + type = 'c'; + routeConfig = { name: item.name }; } else { - if (searchType.get() === 'channels') { - type = 'c'; - routeConfig = { name: item.name }; - } else { - type = 'd'; - routeConfig = { name: item.username }; - } - roomTypes.openRouteLink(type, routeConfig); + type = 'd'; + routeConfig = { name: item.username }; } + roomTypes.openRouteLink(type, routeConfig); }; }, isLoading() { @@ -245,38 +232,6 @@ Template.directory.onRendered(function() { this.isLoading.set(false); this.end.set(!result); - // If there is no result, searching every workspace and - // the search text is an email address, try to find a federated user - if (this.searchWorkspace.get() === 'external' && this.searchText.get().indexOf('@') !== -1) { - const email = this.searchText.get(); - - Meteor.call('federationSearchUsers', email, (error, federatedUsers) => { - if (!federatedUsers) { return; } - - result = result || []; - - for (const federatedUser of federatedUsers) { - const { user } = federatedUser; - - const exists = result.findIndex((e) => e.domain === user.federation.peer && e.username === user.username) !== -1; - - if (exists) { continue; } - - // Add the federated user to the results - result.unshift({ - remoteOnly: true, - name: user.name, - username: user.username, - email: user.emails && user.emails[0] && user.emails[0].address, - createdAt: timeAgo(user.createdAt, t), - domain: user.federation.peer, - }); - } - - setResults.call(this, result); - }); - } - setResults.call(this, result); }); }); From 8bdb7d23f5b711a2c68bef4363371d9c949389d9 Mon Sep 17 00:00:00 2001 From: Alan Sikora Date: Thu, 28 Mar 2019 15:47:40 -0300 Subject: [PATCH 6/8] missing file after git case problem --- server/methods/browseChannels.js | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/server/methods/browseChannels.js b/server/methods/browseChannels.js index 12952f92be9d..e23f7ed2d11b 100644 --- a/server/methods/browseChannels.js +++ b/server/methods/browseChannels.js @@ -115,9 +115,34 @@ Meteor.methods({ result = Users.findByActiveLocalUsersExcept(text, exceptions, options, forcedSearchFields, Federation.localIdentifier); } + 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); + + 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; } + + // Add the federated user to the results + results.unshift({ + username: user.username, + name: user.name, + createdAt: user.createdAt, + emails: user.emails, + federation: user.federation, + }); + } + } + return { - total: result.count(), // count ignores the `skip` and `limit` options - results: result.fetch(), + total, + results, }; }, }); From 1136f55cd62c81170bfa11134fe8f0730de0ab50 Mon Sep 17 00:00:00 2001 From: Alan Sikora Date: Thu, 28 Mar 2019 16:40:36 -0300 Subject: [PATCH 7/8] missing file after git case problem --- server/methods/createDirectMessage.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/server/methods/createDirectMessage.js b/server/methods/createDirectMessage.js index 396e15590f9f..b3f1e637bdbd 100644 --- a/server/methods/createDirectMessage.js +++ b/server/methods/createDirectMessage.js @@ -7,6 +7,8 @@ import { getDefaultSubscriptionPref } from '../../app/utils'; import { RateLimiter } from '../../app/lib'; import { callbacks } from '../../app/callbacks'; +import { Federation } from '../../app/federation/server'; + Meteor.methods({ createDirectMessage(username) { check(username, String); @@ -37,7 +39,14 @@ Meteor.methods({ }); } - const to = Users.findOneByUsername(username); + let to = Users.findOneByUsername(username); + + 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); + } if (!to) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { From f38ca7f2f9824c3dd08fbeac1eb878a3aff68ca9 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 28 Mar 2019 18:35:56 -0300 Subject: [PATCH 8/8] Don't bind --- app/federation/server/index.js | 4 ++-- app/federation/server/methods/addUser.js | 5 +++-- app/federation/server/methods/searchUsers.js | 6 ++++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/federation/server/index.js b/app/federation/server/index.js index 67afc5ce3081..870d70ca20f2 100644 --- a/app/federation/server/index.js +++ b/app/federation/server/index.js @@ -37,8 +37,8 @@ export const Federation = { // Add Federation methods with bound context Federation.methods = { - addUser: addUser.bind(Federation), - searchUsers: searchUsers.bind(Federation), + addUser, + searchUsers, }; // Generate keys diff --git a/app/federation/server/methods/addUser.js b/app/federation/server/methods/addUser.js index bd87b5ee6147..45f83096d67b 100644 --- a/app/federation/server/methods/addUser.js +++ b/app/federation/server/methods/addUser.js @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Users } from '../../../models'; +import { Federation } from '../'; import { logger } from '../logger'; export function addUser(identifier) { @@ -8,12 +9,12 @@ export function addUser(identifier) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'Federation.addUser' }); } - if (!this.peerServer.enabled) { + 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] = this.peerClient.findUsers(identifier, { usernameOnly: true }); + const [federatedUser] = Federation.peerClient.findUsers(identifier, { usernameOnly: true }); if (!federatedUser) { throw new Meteor.Error('federation-invalid-user', 'There is no user to add.'); diff --git a/app/federation/server/methods/searchUsers.js b/app/federation/server/methods/searchUsers.js index 5dade6646f75..7fe711f45b23 100644 --- a/app/federation/server/methods/searchUsers.js +++ b/app/federation/server/methods/searchUsers.js @@ -1,15 +1,17 @@ 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 (!this.peerClient.enabled) { + if (!Federation.peerClient.enabled) { throw new Meteor.Error('error-federation-disabled', 'Federation disabled', { method: 'federationSearchUsers' }); } - const federatedUsers = this.peerClient.findUsers(identifier); + const federatedUsers = Federation.peerClient.findUsers(identifier); if (!federatedUsers.length) { throw new Meteor.Error('federation-user-not-found', `Could not find federated users using "${ identifier }"`);