diff --git a/apps/meteor/ee/server/startup/federation.ts b/apps/meteor/ee/server/startup/federation.ts index 12b4df1a8a370..47636b6cc2b12 100644 --- a/apps/meteor/ee/server/startup/federation.ts +++ b/apps/meteor/ee/server/startup/federation.ts @@ -10,13 +10,45 @@ 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 => { let federationMatrixService: FederationMatrix | undefined; const shouldStartService = (): boolean => { const hasLicense = License.hasModule('federation'); const isEnabled = settings.get('Federation_Service_Enabled') === true; - return hasLicense && isEnabled; + const domain = settings.get('Federation_Service_Domain'); + const hasDomain = validateDomain(domain); + return hasLicense && isEnabled && hasDomain; }; const startService = async (): Promise => { @@ -88,4 +120,16 @@ export const startFederationService = async (): Promise => { await stopService(); } }); + + settings.watch('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(); + } + }); }; diff --git a/apps/meteor/server/settings/federation-service.ts b/apps/meteor/server/settings/federation-service.ts index 797aa75cd395e..8bff1f9d25f57 100644 --- a/apps/meteor/server/settings/federation-service.ts +++ b/apps/meteor/server/settings/federation-service.ts @@ -11,6 +11,15 @@ export const createFederationServiceSettings = async (): Promise => { alert: 'Federation_Service_Alert', }); + await this.add('Federation_Service_Domain', '', { + type: 'string', + public: false, + enterprise: true, + modules: ['federation'], + invalidValue: '', + alert: 'Federation_Service_Domain_Alert', + }); + await this.add('Federation_Service_Matrix_Signing_Algorithm', 'ed25519', { type: 'select', public: false, diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index cf9f095b3364d..884c3192389ab 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -70,10 +70,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS const settingsSigningAlg = await Settings.get('Federation_Service_Matrix_Signing_Algorithm'); const settingsSigningVersion = await Settings.get('Federation_Service_Matrix_Signing_Version'); const settingsSigningKey = await Settings.get('Federation_Service_Matrix_Signing_Key'); - - const siteUrl = await Settings.get('Site_Url'); - - const serverHostname = new URL(siteUrl).hostname; + const serverHostname = (await Settings.get('Federation_Service_Domain')).trim(); instance.serverName = serverHostname; @@ -168,6 +165,8 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS }, ); + instance.logger.startup(`Federation Matrix Homeserver created for domain ${instance.serverName}`); + return instance; } @@ -205,6 +204,10 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS return this.httpRoutes; } + getServerName(): string { + return this.serverName; + } + async createRoom(room: IRoom, owner: IUser, members: string[]): Promise<{ room_id: string; event_id: string }> { if (!this.homeserverServices) { this.logger.warn('Homeserver services not available, skipping room creation'); diff --git a/ee/packages/federation-matrix/src/api/.well-known/server.ts b/ee/packages/federation-matrix/src/api/.well-known/server.ts index 0ab75752a29ce..3b249f23783f8 100644 --- a/ee/packages/federation-matrix/src/api/.well-known/server.ts +++ b/ee/packages/federation-matrix/src/api/.well-known/server.ts @@ -17,6 +17,8 @@ const WellKnownServerResponseSchema = { const isWellKnownServerResponseProps = ajv.compile(WellKnownServerResponseSchema); +// TODO: After changing the domain setting this route is still reporting the old domain until the server is restarted +// TODO: this is wrong, is siteurl !== domain this path should return 404. this path is to discover the final address, domain being the "proxy" and siteurl the final destination, if domain is different, well-known should be served there, not here. export const getWellKnownRoutes = (services: HomeserverServices) => { const { wellKnown } = services; @@ -28,11 +30,11 @@ export const getWellKnownRoutes = (services: HomeserverServices) => { license: ['federation'] }, async (c) => { const responseData = wellKnown.getWellKnownHostData(); - + const etag = createHash('md5') .update(JSON.stringify(responseData)) .digest('hex'); - + c.header('ETag', etag); c.header('Content-Type', 'application/json'); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 2e06926d04eb0..3bfa4831490b4 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2160,6 +2160,9 @@ "Federation_Service_EDU_Process_Presence_Description": "Send and receive events of user presence (online, offline, etc.) between federated servers.", "Federation_Service_EDU_Process_Presence_Alert": "Enabling presence events may increase the load on your server and network traffic considerably, especially if you have many users. Only enable this option if you understand the implications and have the necessary resources to handle the additional load.", "Federation_Service_Alert": "This is an alfa feature not intended for production usage!
It may not be stable and/or performatic. Please be aware that it may change, break, or even be removed in the future without any notice.", + "Federation_Service_Domain": "Federated Domain", + "Federation_Service_Domain_Description": "The domain that this server should respond to, for example: `acme.com`. This will be used as the suffix for user IDs (e.g., `@user:acme.com`).
If your chat server is accessible from a different domain than the one you want to use for federation, you should follow our documentation to configure the `.well-known` file on your web server.", + "Federation_Service_Domain_Alert": "Inform only the domain, do not include http(s)://, slashes or any path after it.
Use something like `acme.com` and not `https://acme.com/chat`.", "Federation_Service_Matrix_Signing_Algorithm": "Signing Key Algorithm", "Federation_Service_Matrix_Signing_Version": "Signing Key Version", "Federation_Service_Matrix_Signing_Key": "Signing Key",