Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
46 changes: 45 additions & 1 deletion apps/meteor/ee/server/startup/federation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment on lines +16 to +41
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

🧩 Analysis chain

Verify the domain validation requirements against the latest Matrix specification.

The validateDomain function implements several validation rules (trimming, non-empty check, lowercase requirement, URL hostname validation), but it's unclear if these requirements align with the latest Matrix homeserver domain specifications.

Based on the search results, Matrix homeserver domains should match what you've configured in your Matrix configuration file, but the specific validation requirements for domain format aren't clearly documented. Please search for the latest Matrix specification requirements for homeserver domain validation:


🌐 Web query:

Matrix homeserver domain name validation requirements specification RFC

💡 Result:

Key sources that specify Matrix homeserver domain-name validation and requirements:

  • Matrix specification — Server name grammar & rules (server_name = hostname [ ":" port ], hostname may be IPv4/IPv6 literal or DNS name; recommendations on length/case; based on RFC 1123). [1][2]

  • Matrix Server-Server (federation) / discovery rules — how homeserver names are resolved: use /.well-known/matrix/server, then _matrix._tcp SRV, then connect to hostname:8448; certificate and Host header expectations for IP literals vs DNS names. [3]

  • Matrix historical/r0.1.2 federation text describing certificate/Host header handling and resolution order (useful for precise validation behaviour). [4]

  • Related discussion: SVCB/HTTPS records are not currently supported for homeserver discovery (implementation/issue thread). [5]

References

  1. Matrix spec — Appendices: Identifier grammar / Server Name. (spec.matrix.org)
  2. Matrix spec — Appendices (unstable/current). (spec.matrix.org)
  3. Matrix spec — Server-Server API / Server discovery (/.well-known, SRV, port and TLS behaviour). (spec.matrix.org)
  4. Matrix Federation API (historical r0.1.2) — resolution and certificate handling. (spec.matrix.org)
  5. Issue: Matrix does not support SVCB DNS records for homeserver discovery. (github.com)

If you want, I can extract the exact server_name grammar and the TLS/Host-header rules into a short spec excerpt.

Citations:


Align validateDomain with Matrix server_name grammar

Current checks (rejecting uppercase; strict new URL(...).hostname === value) will reject valid Matrix server_name forms (IDNs, optional ports, IPv6 literals). Actions:

  • apps/meteor/ee/server/startup/federation.ts (validateDomain, lines 16-41): remove the uppercase-only rejection; treat hostnames case‑insensitively and normalize to lowercase for storage/compare if needed.
  • Replace the URL equality check with validation that follows Matrix server_name = hostname [ ":" port ] — allow DNS names, IPv4, bracketed IPv6 literals, and optional port; support IDNs by converting to ASCII (punycode) before validation or use a robust host-parser library.
  • Confirm whether the configured value may include a port and ensure port-range validation and TLS/Host-header rules for IP literals match the Matrix Server‑Server spec.
🤖 Prompt for AI Agents
In apps/meteor/ee/server/startup/federation.ts around lines 16-41, the
validateDomain currently rejects uppercase and uses strict new URL(...).hostname
equality which incorrectly rejects valid Matrix server_name forms (IDNs,
optional ports, IPv6 literals); remove the uppercase-only rejection and treat
hostnames case-insensitively (normalize to lowercase for storage/compare), and
replace the URL equality check with a server_name-aware validation: parse
optional ":port" suffix, convert the host part to ASCII/punycode (using Node's
punycode.toASCII or a host-parser library) and validate it as either a DNS name,
IPv4 address, or bracketed IPv6 literal; if a port is present ensure it is an
integer in 1-65535; use a well-tested host parsing/validation library where
possible to handle IDNs and IPv6 correctly and return true only if host (after
punycode) and optional port meet these rules.


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

Comment on lines 44 to 45
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

Prevent broadcast-listener leaks across restarts; remove on stop and register once.

Each stop/start cycle adds another listener; they’re never removed. This can duplicate events and waste memory.

Apply these diffs:

@@
 export const startFederationService = async (): Promise<void> => {
 	let federationMatrixService: FederationMatrix | undefined;
+	let broadcastHandler: ((name: string, eventName: string, args: any[]) => void) | undefined;
@@
-		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'));
-			}
-		});
+		broadcastHandler = (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'));
+			}
+		};
+		// @ts-expect-error: EventEmitter-like API
+		StreamerCentral.on?.('broadcast', broadcastHandler);
@@
 	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);
 
+		if (broadcastHandler) {
+			// @ts-expect-error: removeListener/off exist on StreamerCentral emitter
+			StreamerCentral.removeListener?.('broadcast', broadcastHandler);
+			// @ts-expect-error
+			StreamerCentral.off?.('broadcast', broadcastHandler);
+			broadcastHandler = undefined;
+		}
+
 		await api.destroyService(federationMatrixService);
 		federationMatrixService = undefined;
 	};

Also applies to: 63-74, 82-95

🤖 Prompt for AI Agents
In apps/meteor/ee/server/startup/federation.ts around lines 44-45 (and similarly
for blocks ~63-74 and ~82-95), the broadcast-listener is being added on every
start without being removed on stop, causing duplicate handlers across restart
cycles; fix by registering the listener exactly once and removing it on stop:
keep a single reference to the listener function (or use a boolean guard) when
adding it so repeated starts don’t re-register, and on the stop/shutdown path
call the corresponding removeListener/off with that same function reference (and
clear the reference) so the listener is fully deregistered between restarts.
Ensure the lifecycle uses the same federationMatrixService instance checks
before registering and that cleanup runs unconditionally on stop.

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

const startService = async (): Promise<void> => {
Expand Down Expand Up @@ -88,4 +120,16 @@ export const startFederationService = async (): Promise<void> => {
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();
}
});
};
9 changes: 9 additions & 0 deletions apps/meteor/server/settings/federation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ export const createFederationServiceSettings = async (): Promise<void> => {
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,
Expand Down
11 changes: 7 additions & 4 deletions ee/packages/federation-matrix/src/FederationMatrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
const settingsSigningAlg = await Settings.get<string>('Federation_Service_Matrix_Signing_Algorithm');
const settingsSigningVersion = await Settings.get<string>('Federation_Service_Matrix_Signing_Version');
const settingsSigningKey = await Settings.get<string>('Federation_Service_Matrix_Signing_Key');

const siteUrl = await Settings.get<string>('Site_Url');

const serverHostname = new URL(siteUrl).hostname;
const serverHostname = (await Settings.get<string>('Federation_Service_Domain')).trim();

instance.serverName = serverHostname;

Expand Down Expand Up @@ -168,6 +165,8 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
},
);

instance.logger.startup(`Federation Matrix Homeserver created for domain ${instance.serverName}`);

return instance;
}

Expand Down Expand Up @@ -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');
Expand Down
6 changes: 4 additions & 2 deletions ee/packages/federation-matrix/src/api/.well-known/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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

Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<strong>This is an alfa feature not intended for production usage!</strong><br/>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`).<br/>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.<br/>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",
Expand Down
Loading