diff --git a/packages/federation-sdk/src/index.ts b/packages/federation-sdk/src/index.ts index 3ef2a6288..369758c66 100644 --- a/packages/federation-sdk/src/index.ts +++ b/packages/federation-sdk/src/index.ts @@ -90,6 +90,7 @@ export { type BaseEventType, } from './utils/event-schemas'; export { errCodes } from './utils/response-codes'; +export { NotAllowedError } from './services/invite.service'; export { EventRepository } from './repositories/event.repository'; export { RoomRepository } from './repositories/room.repository'; @@ -163,6 +164,10 @@ export type HomeserverEventSignatures = { last_active_ago?: number; origin?: string; }; + 'homeserver.matrix.encryption': { + event_id: EventID; + event: PduForType<'m.room.encryption'>; + }; 'homeserver.matrix.encrypted': { event_id: EventID; event: PduForType<'m.room.encrypted'>; diff --git a/packages/federation-sdk/src/services/config.service.ts b/packages/federation-sdk/src/services/config.service.ts index 908a02636..5e21b2d29 100644 --- a/packages/federation-sdk/src/services/config.service.ts +++ b/packages/federation-sdk/src/services/config.service.ts @@ -31,6 +31,10 @@ export interface AppConfig { downloadPerMinute: number; }; }; + invite: { + allowedEncryptedRooms: boolean; + allowedNonPrivateRooms: boolean; + }; } export const AppConfigSchema = z.object({ @@ -69,6 +73,10 @@ export const AppConfigSchema = z.object({ .min(1, 'Download rate limit must be at least 1'), }), }), + invite: z.object({ + allowedEncryptedRooms: z.boolean(), + allowedNonPrivateRooms: z.boolean(), + }), }); export class ConfigService { @@ -113,6 +121,10 @@ export class ConfigService { return this.config.media; } + getInviteConfig(): AppConfig['invite'] { + return this.config.invite; + } + async getSigningKey() { // If config contains a signing key, use it if (!this.config.signingKey) { diff --git a/packages/federation-sdk/src/services/invite.service.ts b/packages/federation-sdk/src/services/invite.service.ts index 1e0d46e3a..8ec2bb791 100644 --- a/packages/federation-sdk/src/services/invite.service.ts +++ b/packages/federation-sdk/src/services/invite.service.ts @@ -25,6 +25,13 @@ export type ProcessInviteEvent = { room_version: string; }; +export class NotAllowedError extends Error { + constructor(message: string) { + super(message); + this.name = 'NotAllowedError'; + } +} + @singleton() export class InviteService { private readonly logger = createLogger('InviteService'); @@ -142,6 +149,42 @@ export class InviteService { }; } + private async shouldProcessInvite( + event: PduForType<'m.room.member'>, + ): Promise { + const isRoomNonPrivate = event.unsigned.invite_room_state.some( + ( + stateEvent: PersistentEventBase< + RoomVersion, + 'm.room.join_rules' + >['event'], + ) => + stateEvent.type === 'm.room.join_rules' && + stateEvent.content.join_rule === 'public', + ); + + const isRoomEncrypted = event.unsigned.invite_room_state.some( + ( + stateEvent: PersistentEventBase< + RoomVersion, + 'm.room.encryption' + >['event'], + ) => stateEvent.type === 'm.room.encryption', + ); + + const { allowedEncryptedRooms, allowedNonPrivateRooms } = + this.configService.getInviteConfig(); + + const shouldRejectInvite = + (!allowedEncryptedRooms && isRoomEncrypted) || + (!allowedNonPrivateRooms && isRoomNonPrivate); + if (shouldRejectInvite) { + throw new NotAllowedError( + `Could not process invite due to room being ${isRoomEncrypted ? 'encrypted' : 'public'}`, + ); + } + } + async processInvite( event: PduForType<'m.room.member'>, roomId: RoomID, @@ -150,6 +193,7 @@ export class InviteService { authenticatedServer: string, ) { // SPEC: when a user invites another user on a different homeserver, a request to that homeserver to have the event signed and verified must be made + await this.shouldProcessInvite(event); const residentServer = roomId.split(':').pop(); if (!residentServer) { diff --git a/packages/federation-sdk/src/services/room.service.ts b/packages/federation-sdk/src/services/room.service.ts index 8c7b52013..4f3673d05 100644 --- a/packages/federation-sdk/src/services/room.service.ts +++ b/packages/federation-sdk/src/services/room.service.ts @@ -845,7 +845,7 @@ export class RoomService { // trying to join room from another server const makeJoinResponse = await federationService.makeJoin( - residentServer as string, + residentServer, roomId, userId, roomVersion, // NOTE: check the comment in the called method diff --git a/packages/federation-sdk/src/services/staging-area.service.ts b/packages/federation-sdk/src/services/staging-area.service.ts index 4cde494fc..96894fbcc 100644 --- a/packages/federation-sdk/src/services/staging-area.service.ts +++ b/packages/federation-sdk/src/services/staging-area.service.ts @@ -308,6 +308,12 @@ export class StagingAreaService { }, }); break; + case event.event.type === 'm.room.encryption': + this.eventEmitterService.emit('homeserver.matrix.encryption', { + event_id: eventId, + event: event.event, + }); + break; case event.event.type === 'm.room.encrypted': this.eventEmitterService.emit('homeserver.matrix.encrypted', { event_id: eventId, diff --git a/packages/homeserver/src/controllers/federation/invite.controller.ts b/packages/homeserver/src/controllers/federation/invite.controller.ts index 0f81c0ca2..22359f183 100644 --- a/packages/homeserver/src/controllers/federation/invite.controller.ts +++ b/packages/homeserver/src/controllers/federation/invite.controller.ts @@ -2,11 +2,17 @@ import { EventID, RoomID } from '@rocket.chat/federation-room'; import { EventAuthorizationService, InviteService, + NotAllowedError, } from '@rocket.chat/federation-sdk'; import { isAuthenticatedMiddleware } from '@rocket.chat/homeserver/middlewares/isAuthenticated'; import { Elysia, t } from 'elysia'; import { container } from 'tsyringe'; -import { ProcessInviteParamsDto, RoomVersionDto } from '../../dtos'; +import { + FederationErrorResponseDto, + ProcessInviteParamsDto, + ProcessInviteResponseDto, + RoomVersionDto, +} from '../../dtos'; export const invitePlugin = (app: Elysia) => { const inviteService = container.resolve(InviteService); @@ -14,18 +20,38 @@ export const invitePlugin = (app: Elysia) => { return app.use(isAuthenticatedMiddleware(eventAuthService)).put( '/_matrix/federation/v2/invite/:roomId/:eventId', - async ({ body, params: { roomId, eventId }, authenticatedServer }) => { + async ({ body, set, params: { roomId, eventId }, authenticatedServer }) => { if (!authenticatedServer) { throw new Error('Missing authenticated server from request'); } - return inviteService.processInvite( - body.event, - roomId as RoomID, - eventId as EventID, - body.room_version, - authenticatedServer, - ); + try { + return await inviteService.processInvite( + body.event, + roomId as RoomID, + eventId as EventID, + body.room_version, + authenticatedServer, + ); + } catch (error) { + if (error instanceof NotAllowedError) { + set.status = 403; + return { + errcode: 'M_FORBIDDEN', + error: + 'This server does not allow joining this type of room based on federation settings.', + }; + } + + set.status = 500; + return { + errcode: 'M_UNKNOWN', + error: + error instanceof Error + ? error.message + : 'Internal server error while processing request', + }; + } }, { params: ProcessInviteParamsDto, @@ -34,6 +60,11 @@ export const invitePlugin = (app: Elysia) => { room_version: RoomVersionDto, invite_room_state: t.Any(), }), + response: { + 200: ProcessInviteResponseDto, + 403: FederationErrorResponseDto, + 500: FederationErrorResponseDto, + }, detail: { tags: ['Federation'], summary: 'Process room invite', diff --git a/packages/homeserver/src/dtos/federation/error.dto.ts b/packages/homeserver/src/dtos/federation/error.dto.ts new file mode 100644 index 000000000..2b9674a1c --- /dev/null +++ b/packages/homeserver/src/dtos/federation/error.dto.ts @@ -0,0 +1,15 @@ +import { type Static, t } from 'elysia'; + +export const FederationErrorResponseDto = t.Object({ + errcode: t.Enum({ + M_UNRECOGNIZED: 'M_UNRECOGNIZED', + M_UNAUTHORIZED: 'M_UNAUTHORIZED', + M_FORBIDDEN: 'M_FORBIDDEN', + M_UNKNOWN: 'M_UNKNOWN', + }), + error: t.String(), +}); + +export type FederationErrorResponseDto = Static< + typeof FederationErrorResponseDto +>; diff --git a/packages/homeserver/src/dtos/index.ts b/packages/homeserver/src/dtos/index.ts index c1143a5fa..b0d13338a 100644 --- a/packages/homeserver/src/dtos/index.ts +++ b/packages/homeserver/src/dtos/index.ts @@ -12,6 +12,7 @@ export * from './federation/state-ids.dto'; export * from './federation/state.dto'; export * from './federation/transactions.dto'; export * from './federation/versions.dto'; +export * from './federation/error.dto'; // Internal DTOs export * from './internal/invite.dto'; diff --git a/packages/homeserver/src/homeserver.module.ts b/packages/homeserver/src/homeserver.module.ts index 3710c7b1b..c5665ef8b 100644 --- a/packages/homeserver/src/homeserver.module.ts +++ b/packages/homeserver/src/homeserver.module.ts @@ -88,6 +88,12 @@ export async function setup(options?: HomeserverSetupOptions) { ), }, }, + invite: { + allowedEncryptedRooms: + process.env.INVITE_ALLOWED_ENCRYPTED_ROOMS === 'true', + allowedNonPrivateRooms: + process.env.INVITE_ALLOWED_NON_PRIVATE_ROOMS === 'true', + }, }); const containerOptions: FederationContainerOptions = { diff --git a/packages/room/src/types/v3-11.ts b/packages/room/src/types/v3-11.ts index 1e2b8321e..d0ae6cdd9 100644 --- a/packages/room/src/types/v3-11.ts +++ b/packages/room/src/types/v3-11.ts @@ -537,6 +537,13 @@ const EncryptedContentSchema = BaseTimelineContentSchema.extend({ .optional(), }); +export const PduEncryptionEventContentSchema = z.object({ + algorithm: z + .enum(['m.megolm.v1.aes-sha2']) + .describe('The algorithm used to encrypt the content.'), + ciphertext: z.string().describe('The encrypted content.'), +}); + export type PduMessageEventContent = z.infer< typeof PduMessageEventContentSchema >; @@ -706,6 +713,12 @@ const EventPduTypeRoomEncrypted = z.object({ content: EncryptedContentSchema, }); +const EventPduTypeRoomEncryption = z.object({ + ...PduNoContentEmptyStateKeyStateEventSchema, + type: z.literal('m.room.encryption'), + content: PduEncryptionEventContentSchema, +}); + const EventPduTypeRoomMessage = z.object({ ...PduNoContentTimelineEventSchema, type: z.literal('m.room.message'), @@ -749,6 +762,8 @@ export const PduStateEventSchema = z.discriminatedUnion('type', [ EventPduTypeRoomServerAcl, EventPduTypeRoomTombstone, + + EventPduTypeRoomEncryption, ]); export const PduTimelineSchema = z.discriminatedUnion('type', [