Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 7 additions & 17 deletions apps/meteor/ee/server/api/federation.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import type { IFederationMatrixService } from '@rocket.chat/core-services';
import { FederationMatrix } from '@rocket.chat/core-services';
import { getFederationRoutes } from '@rocket.chat/federation-matrix';
import { Logger } from '@rocket.chat/logger';
import { ajv } from '@rocket.chat/rest-typings';
import type express from 'express';
import { WebApp } from 'meteor/webapp';

import { API } from '../../../app/api/server';
import { isRunningMs } from '../../../server/lib/isRunningMs';

const logger = new Logger('FederationRoutes');

let federationService: IFederationMatrixService | undefined;
API.v1.get(
'/federation/matrixIds.verify',
{
Expand All @@ -35,28 +34,19 @@ API.v1.get(
},
async function () {
const { matrixIds } = this.queryParams;
if (!federationService) {
throw new Error('Federation service not registered');
}
return API.v1.success({
results: await federationService.verifyMatrixIds(matrixIds),
results: await FederationMatrix.verifyMatrixIds(matrixIds),
});
},
);

export async function registerFederationRoutes(f: IFederationMatrixService): Promise<void> {
federationService = f;
if (isRunningMs()) {
return;
}

export async function registerFederationRoutes(): Promise<void> {
try {
const routes = federationService.getAllRoutes();
(WebApp.rawConnectHandlers as unknown as ReturnType<typeof express>).use(routes.matrix.router).use(routes.wellKnown.router);
const routes = getFederationRoutes();

logger.log('[Federation] Registered federation routes');
(WebApp.rawConnectHandlers as unknown as ReturnType<typeof express>).use(routes.matrix.router).use(routes.wellKnown.router);
} catch (error) {
logger.error('[Federation] Failed to register routes:', error);
logger.error({ msg: '[Federation] Failed to register routes:', err: error });
throw error;
}
}
135 changes: 15 additions & 120 deletions apps/meteor/ee/server/startup/federation.ts
Original file line number Diff line number Diff line change
@@ -1,135 +1,30 @@
import { api } from '@rocket.chat/core-services';
import { FederationMatrix } from '@rocket.chat/federation-matrix';
import { api, FederationMatrix as FederationMatrixService } from '@rocket.chat/core-services';
import { FederationMatrix, setupFederationMatrix } from '@rocket.chat/federation-matrix';
import { InstanceStatus } from '@rocket.chat/instance-status';
import { License } from '@rocket.chat/license';
import { Logger } from '@rocket.chat/logger';

import { settings } from '../../../app/settings/server';
import { StreamerCentral } from '../../../server/modules/streamer/streamer.module';
import { registerFederationRoutes } from '../api/federation';

const logger = new Logger('Federation');

// TODO: should validate if the domain is resolving to us or not correctly
// should use homeserver.getFinalSomethingSomething and validate final Host header to have siteUrl
// this is a minimum sanity check to avoid full urls instead of the expected domain part
function validateDomain(domain: string): boolean {
const value = domain.trim();

if (!value) {
logger.error('The Federation domain is not set');
return false;
}

if (value.toLowerCase() !== value) {
logger.error(`The Federation domain "${value}" cannot have uppercase letters`);
return false;
}

try {
const valid = new URL(`https://${value}`).hostname === value;

if (!valid) {
throw new Error();
}
} catch {
logger.error(`The configured Federation domain "${value}" is not valid`);
return false;
}

return true;
}

export const startFederationService = async (): Promise<void> => {
let federationMatrixService: FederationMatrix | undefined;

const shouldStartService = (): boolean => {
const hasLicense = License.hasModule('federation');
const isEnabled = settings.get('Federation_Service_Enabled') === true;
const domain = settings.get<string>('Federation_Service_Domain');
const hasDomain = validateDomain(domain);
return hasLicense && isEnabled && hasDomain;
};

const startService = async (): Promise<void> => {
if (federationMatrixService) {
logger.debug('Federation-matrix service already started... skipping');
return;
}

logger.debug('Starting federation-matrix service');
federationMatrixService = await FederationMatrix.create(InstanceStatus.id());

StreamerCentral.on('broadcast', (name, eventName, args) => {
if (!federationMatrixService) {
return;
}
if (name === 'notify-room' && eventName.endsWith('user-activity')) {
const [rid] = eventName.split('/');
const [user, activity] = args;
void federationMatrixService.notifyUserTyping(rid, user, activity.includes('user-typing'));
}
});

try {
api.registerService(federationMatrixService);
await registerFederationRoutes(federationMatrixService);
} catch (error) {
logger.error('Failed to start federation-matrix service:', error);
}
};

const stopService = async (): Promise<void> => {
if (!federationMatrixService) {
logger.debug('Federation-matrix service not registered... skipping');
return;
}

logger.debug('Stopping federation-matrix service');

// TODO: Unregister routes
// await unregisterFederationRoutes(federationMatrixService);
await setupFederationMatrix(InstanceStatus.id());

await api.destroyService(federationMatrixService);
federationMatrixService = undefined;
};
api.registerService(new FederationMatrix());

if (shouldStartService()) {
await startService();
}

void License.onLicense('federation', async () => {
logger.debug('Federation license became available');
if (shouldStartService()) {
await startService();
// TODO move to service/setup?
StreamerCentral.on('broadcast', (name, eventName, args) => {
if (name === 'notify-room' && eventName.endsWith('user-activity')) {
const [rid] = eventName.split('/');
const [user, activity] = args;
void FederationMatrixService.notifyUserTyping(rid, user, activity.includes('user-typing'));
}
});

License.onInvalidateLicense(async () => {
logger.debug('License invalidated, checking federation module');
if (!shouldStartService()) {
await stopService();
}
});

settings.watch('Federation_Service_Enabled', async (enabled) => {
logger.debug('Federation_Service_Enabled setting changed:', enabled);
if (shouldStartService()) {
await startService();
} else {
await stopService();
}
});

settings.watch<string>('Federation_Service_Domain', async (domain) => {
logger.debug('Federation_Service_Domain setting changed:', domain);
if (shouldStartService()) {
if (domain.toLowerCase() !== federationMatrixService?.getServerName().toLowerCase()) {
await stopService();
}
await startService();
} else {
await stopService();
}
});
try {
await registerFederationRoutes();
} catch (error) {
logger.error('Failed to start federation-matrix service:', error);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't run the rest of startup when federation is disabled

setupFederationMatrix returns before calling createFederationContainer whenever the module is off (no license or the Federation_Service_Enabled flag is false). We still instantiate FederationMatrix and call registerFederationRoutes(). Both paths hit getAllServices(), which throws “Federation container not initialized”, so every non-federated deployment now logs “Failed to start federation-matrix service” during boot. Please short-circuit after setup and skip service/route registration when it reports disabled. For example:

-	await setupFederationMatrix(InstanceStatus.id());
-
-	api.registerService(new FederationMatrix());
+	const isEnabled = await setupFederationMatrix(InstanceStatus.id());
+	if (!isEnabled) {
+		logger.info('Federation service disabled; skipping federation-matrix startup');
+		return;
+	}
+
+	api.registerService(new FederationMatrix());

(This requires setupFederationMatrix to return that boolean.) Based on ee/packages/federation-matrix/src/setup.ts.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/meteor/ee/server/startup/federation.ts around lines 12 to 29,
setupFederationMatrix can return early when federation is disabled, but the
current code continues and instantiates services/routes causing errors; update
the startup to inspect the boolean return value from await
setupFederationMatrix(InstanceStatus.id()) and if it returns false, immediately
return (skip api.registerService, the StreamerCentral broadcast handler, and
registerFederationRoutes calls) so service/route registration only happens when
federation is enabled.

};
1 change: 0 additions & 1 deletion apps/meteor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,6 @@
"@rocket.chat/emitter": "~0.31.25",
"@rocket.chat/favicon": "workspace:^",
"@rocket.chat/federation-matrix": "workspace:^",
"@rocket.chat/federation-service": "workspace:^",
"@rocket.chat/freeswitch": "workspace:^",
"@rocket.chat/fuselage": "^0.66.4",
"@rocket.chat/fuselage-forms": "^0.1.0",
Expand Down
4 changes: 0 additions & 4 deletions ee/apps/federation-service/.eslintrc.json

This file was deleted.

33 changes: 0 additions & 33 deletions ee/apps/federation-service/CHANGELOG.md

This file was deleted.

53 changes: 0 additions & 53 deletions ee/apps/federation-service/package.json

This file was deleted.

3 changes: 0 additions & 3 deletions ee/apps/federation-service/src/config.ts

This file was deleted.

71 changes: 0 additions & 71 deletions ee/apps/federation-service/src/service.ts

This file was deleted.

Loading
Loading