diff --git a/apps/meteor/server/settings/federation-service.ts b/apps/meteor/server/settings/federation-service.ts index 65174cf6f008d..3a47d15751d95 100644 --- a/apps/meteor/server/settings/federation-service.ts +++ b/apps/meteor/server/settings/federation-service.ts @@ -24,5 +24,12 @@ export const createFederationServiceSettings = async (): Promise => { i18nDescription: 'Federation_Service_Matrix_Signing_Key_Description', public: false, }); + + await this.add('Federation_Service_Allow_List', '', { + type: 'string', + i18nLabel: 'Federation_Service_Allow_List', + i18nDescription: 'Federation_Service_Allow_List_Description', + public: false, + }); }); }; diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 05beb0cf9ab05..cd54cfd8646ed 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -21,6 +21,7 @@ import { getMatrixRoomsRoutes } from './api/_matrix/rooms'; import { getMatrixSendJoinRoutes } from './api/_matrix/send-join'; import { getMatrixTransactionsRoutes } from './api/_matrix/transactions'; import { getFederationVersionsRoutes } from './api/_matrix/versions'; +import { isFederationDomainAllowedMiddleware } from './api/middlewares/isFederationDomainAllowed'; import { isFederationEnabledMiddleware } from './api/middlewares/isFederationEnabled'; import { isLicenseEnabledMiddleware } from './api/middlewares/isLicenseEnabled'; import { registerEvents } from './events'; @@ -164,6 +165,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS matrix .use(isFederationEnabledMiddleware) .use(isLicenseEnabledMiddleware) + .use(isFederationDomainAllowedMiddleware) .use(getMatrixInviteRoutes(this.homeserverServices)) .use(getMatrixProfilesRoutes(this.homeserverServices)) .use(getMatrixRoomsRoutes(this.homeserverServices)) diff --git a/ee/packages/federation-matrix/src/api/middlewares/isFederationDomainAllowed.ts b/ee/packages/federation-matrix/src/api/middlewares/isFederationDomainAllowed.ts new file mode 100644 index 0000000000000..78c145b695e84 --- /dev/null +++ b/ee/packages/federation-matrix/src/api/middlewares/isFederationDomainAllowed.ts @@ -0,0 +1,61 @@ +import { Settings } from '@rocket.chat/core-services'; +import { createMiddleware } from 'hono/factory'; +import mem from 'mem'; + +// cache for 60 seconds +const getAllowList = mem( + async () => { + const allowListSetting = await Settings.get('Federation_Service_Allow_List'); + return allowListSetting + ? allowListSetting + .split(',') + .map((d) => d.trim().toLowerCase()) + .filter(Boolean) + : null; + }, + { maxAge: 60000 }, +); + +/** + * Parses all key-value pairs from a Matrix authorization header. + * Example: X-Matrix origin="matrix.org", key="value", ... + * Returns an object with all parsed values. + */ +// TODO make this function more of a utility if needed elsewhere +function parseMatrixAuthorizationHeader(header: string): Record { + const result: Record = {}; + // Match key="value" pairs + const regex = /([a-zA-Z0-9_-]+)\s*=\s*"([^"]*)"/g; + let match; + while ((match = regex.exec(header)) !== null) { + result[match[1]] = match[2]; + } + return result; +} + +export const isFederationDomainAllowedMiddleware = createMiddleware(async (c, next) => { + const allowList = await getAllowList(); + if (!allowList || allowList.length === 0) { + // No restriction, allow all + return next(); + } + + // Extract all key-value pairs from Matrix authorization header + const authHeader = c.req.header('authorization'); + if (!authHeader) { + return c.json({ errcode: 'M_UNAUTHORIZED', error: 'Missing Authorization headers.' }, 401); + } + + const authValues = parseMatrixAuthorizationHeader(authHeader); + const domain = authValues.origin?.toLowerCase(); + if (!domain) { + return c.json({ errcode: 'M_MISSING_ORIGIN', error: 'Missing origin in authorization header.' }, 401); + } + + // Check if domain is in allowed list + if (allowList.some((allowed) => domain.endsWith(allowed))) { + return next(); + } + + return c.json({ errcode: 'M_FORBIDDEN', error: 'Federation from this domain is not allowed.' }, 403); +}); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index f10abccb024c5..a62e60c67e7a9 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2157,6 +2157,8 @@ "Federation_Service_Alert": "This feature is in beta and may not be stable. Please be aware that it may change, break, or even be removed in the future without any notice.", "Federation_Service_Matrix_Signing_Key": "Matrix server signing key", "Federation_Service_Matrix_Signing_Key_Description": "The private signing key used by your Matrix server to authenticate federation requests. Format should be: algorithm version base64. This is typically an Ed25519 algorithm key (version 4), encoded as base64. It is essential for secure communication between federated Matrix servers and should be kept confidential.", + "Federation_Service_Allow_List": "Domain Allow List", + "Federation_Service_Allow_List_Description": "Restrict federation to the given allow list of domains.", "Field": "Field", "Field_removed": "Field removed", "Field_required": "Field required", @@ -7023,4 +7025,4 @@ "__usernames__joined": "{{usernames}} joined", "__usersCount__joined": "{{count}} joined", "__usersCount__people_will_be_invited": "{{usersCount}} people will be invited" -} \ No newline at end of file +}