Improve: Support search and adding federated users through regular endpoints (#13936)

pull/13820/head^2
Alan Sikora 7 years ago committed by Diego Sampaio
parent be8ae64209
commit 65b5be3a83
  1. 28
      app/federation/server/PeerClient.js
  2. 10
      app/federation/server/PeerDNS.js
  3. 100
      app/federation/server/PeerHTTP/PeerHTTP.js
  4. 1
      app/federation/server/PeerHTTP/index.js
  5. 19
      app/federation/server/PeerHTTP/utils.js
  6. 28
      app/federation/server/PeerServer/PeerServer.js
  7. 4
      app/federation/server/PeerServer/index.js
  8. 41
      app/federation/server/PeerServer/routes/events.js
  9. 4
      app/federation/server/PeerServer/routes/uploads.js
  10. 14
      app/federation/server/PeerServer/routes/users.js
  11. 15
      app/federation/server/federatedResources/FederatedMessage.js
  12. 4
      app/federation/server/federatedResources/FederatedResource.js
  13. 14
      app/federation/server/federatedResources/FederatedRoom.js
  14. 6
      app/federation/server/federatedResources/FederatedUser.js
  15. 8
      app/federation/server/federatedResources/index.js
  16. 28
      app/federation/server/index.js
  17. 45
      app/federation/server/methods/addUser.js
  18. 48
      app/federation/server/methods/federationAddUser.js
  19. 24
      app/federation/server/methods/federationSearchUsers.js
  20. 2
      app/federation/server/methods/index.js
  21. 21
      app/federation/server/methods/searchUsers.js
  22. 126
      app/federation/server/peerHTTP.js
  23. 57
      app/ui/client/views/app/directory.js
  24. 29
      server/methods/browseChannels.js
  25. 11
      server/methods/createDirectMessage.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();

@ -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();

@ -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);
}
}
}
}

@ -0,0 +1 @@
export { PeerHTTP } from './PeerHTTP';

@ -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);
});

@ -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();

@ -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';

@ -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');
}

@ -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');
}

@ -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 },

@ -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;

@ -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;

@ -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;

@ -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;

@ -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';

@ -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,
searchUsers,
};
// Generate keys

@ -0,0 +1,45 @@
import { Meteor } from 'meteor/meteor';
import { Users } from '../../../models';
import { Federation } from '../';
import { logger } from '../logger';
export function addUser(identifier) {
if (!Meteor.userId()) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'Federation.addUser' });
}
if (!Federation.peerServer.enabled) {
throw new Meteor.Error('error-federation-disabled', 'Federation disabled', { method: 'Federation.addUser' });
}
// Make sure the federated user still exists, and get the unique one, by email address
const [federatedUser] = Federation.peerClient.findUsers(identifier, { usernameOnly: true });
if (!federatedUser) {
throw new Meteor.Error('federation-invalid-user', 'There is no user to add.');
}
let user = null;
const localUser = federatedUser.getLocalUser();
localUser.name += `@${ federatedUser.user.federation.peer }`;
// Delete the _id
delete localUser._id;
try {
// Create the local user
user = Users.create(localUser);
} 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;
}

@ -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;
},
});

@ -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;
},
});

@ -1,2 +0,0 @@
import './federationSearchUsers';
import './federationAddUser';

@ -0,0 +1,21 @@
import { Meteor } from 'meteor/meteor';
import { Federation } from '../';
export function searchUsers(identifier) {
if (!Meteor.userId()) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'federationSearchUsers' });
}
if (!Federation.peerClient.enabled) {
throw new Meteor.Error('error-federation-disabled', 'Federation disabled', { method: 'federationSearchUsers' });
}
const federatedUsers = Federation.peerClient.findUsers(identifier);
if (!federatedUsers.length) {
throw new Meteor.Error('federation-user-not-found', `Could not find federated users using "${ identifier }"`);
}
return federatedUsers;
}

@ -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();

@ -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);
});
});

@ -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,
};
},
});

@ -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', {

Loading…
Cancel
Save