Skip to content

Commit 72ec1a0

Browse files
authored
fix(federation): allow to join non-private or encrypted rooms based on settings (#280)
1 parent 2a0357e commit 72ec1a0

File tree

10 files changed

+145
-10
lines changed

10 files changed

+145
-10
lines changed

packages/federation-sdk/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export {
9090
type BaseEventType,
9191
} from './utils/event-schemas';
9292
export { errCodes } from './utils/response-codes';
93+
export { NotAllowedError } from './services/invite.service';
9394

9495
export { EventRepository } from './repositories/event.repository';
9596
export { RoomRepository } from './repositories/room.repository';
@@ -163,6 +164,10 @@ export type HomeserverEventSignatures = {
163164
last_active_ago?: number;
164165
origin?: string;
165166
};
167+
'homeserver.matrix.encryption': {
168+
event_id: EventID;
169+
event: PduForType<'m.room.encryption'>;
170+
};
166171
'homeserver.matrix.encrypted': {
167172
event_id: EventID;
168173
event: PduForType<'m.room.encrypted'>;

packages/federation-sdk/src/services/config.service.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export interface AppConfig {
3131
downloadPerMinute: number;
3232
};
3333
};
34+
invite: {
35+
allowedEncryptedRooms: boolean;
36+
allowedNonPrivateRooms: boolean;
37+
};
3438
}
3539

3640
export const AppConfigSchema = z.object({
@@ -69,6 +73,10 @@ export const AppConfigSchema = z.object({
6973
.min(1, 'Download rate limit must be at least 1'),
7074
}),
7175
}),
76+
invite: z.object({
77+
allowedEncryptedRooms: z.boolean(),
78+
allowedNonPrivateRooms: z.boolean(),
79+
}),
7280
});
7381

7482
export class ConfigService {
@@ -113,6 +121,10 @@ export class ConfigService {
113121
return this.config.media;
114122
}
115123

124+
getInviteConfig(): AppConfig['invite'] {
125+
return this.config.invite;
126+
}
127+
116128
async getSigningKey() {
117129
// If config contains a signing key, use it
118130
if (!this.config.signingKey) {

packages/federation-sdk/src/services/invite.service.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ export type ProcessInviteEvent = {
2525
room_version: string;
2626
};
2727

28+
export class NotAllowedError extends Error {
29+
constructor(message: string) {
30+
super(message);
31+
this.name = 'NotAllowedError';
32+
}
33+
}
34+
2835
@singleton()
2936
export class InviteService {
3037
private readonly logger = createLogger('InviteService');
@@ -142,6 +149,42 @@ export class InviteService {
142149
};
143150
}
144151

152+
private async shouldProcessInvite(
153+
event: PduForType<'m.room.member'>,
154+
): Promise<void> {
155+
const isRoomNonPrivate = event.unsigned.invite_room_state.some(
156+
(
157+
stateEvent: PersistentEventBase<
158+
RoomVersion,
159+
'm.room.join_rules'
160+
>['event'],
161+
) =>
162+
stateEvent.type === 'm.room.join_rules' &&
163+
stateEvent.content.join_rule === 'public',
164+
);
165+
166+
const isRoomEncrypted = event.unsigned.invite_room_state.some(
167+
(
168+
stateEvent: PersistentEventBase<
169+
RoomVersion,
170+
'm.room.encryption'
171+
>['event'],
172+
) => stateEvent.type === 'm.room.encryption',
173+
);
174+
175+
const { allowedEncryptedRooms, allowedNonPrivateRooms } =
176+
this.configService.getInviteConfig();
177+
178+
const shouldRejectInvite =
179+
(!allowedEncryptedRooms && isRoomEncrypted) ||
180+
(!allowedNonPrivateRooms && isRoomNonPrivate);
181+
if (shouldRejectInvite) {
182+
throw new NotAllowedError(
183+
`Could not process invite due to room being ${isRoomEncrypted ? 'encrypted' : 'public'}`,
184+
);
185+
}
186+
}
187+
145188
async processInvite(
146189
event: PduForType<'m.room.member'>,
147190
roomId: RoomID,
@@ -150,6 +193,7 @@ export class InviteService {
150193
authenticatedServer: string,
151194
) {
152195
// 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
196+
await this.shouldProcessInvite(event);
153197

154198
const residentServer = roomId.split(':').pop();
155199
if (!residentServer) {

packages/federation-sdk/src/services/room.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -845,7 +845,7 @@ export class RoomService {
845845

846846
// trying to join room from another server
847847
const makeJoinResponse = await federationService.makeJoin(
848-
residentServer as string,
848+
residentServer,
849849
roomId,
850850
userId,
851851
roomVersion, // NOTE: check the comment in the called method

packages/federation-sdk/src/services/staging-area.service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,12 @@ export class StagingAreaService {
308308
},
309309
});
310310
break;
311+
case event.event.type === 'm.room.encryption':
312+
this.eventEmitterService.emit('homeserver.matrix.encryption', {
313+
event_id: eventId,
314+
event: event.event,
315+
});
316+
break;
311317
case event.event.type === 'm.room.encrypted':
312318
this.eventEmitterService.emit('homeserver.matrix.encrypted', {
313319
event_id: eventId,

packages/homeserver/src/controllers/federation/invite.controller.ts

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,56 @@ import { EventID, RoomID } from '@rocket.chat/federation-room';
22
import {
33
EventAuthorizationService,
44
InviteService,
5+
NotAllowedError,
56
} from '@rocket.chat/federation-sdk';
67
import { isAuthenticatedMiddleware } from '@rocket.chat/homeserver/middlewares/isAuthenticated';
78
import { Elysia, t } from 'elysia';
89
import { container } from 'tsyringe';
9-
import { ProcessInviteParamsDto, RoomVersionDto } from '../../dtos';
10+
import {
11+
FederationErrorResponseDto,
12+
ProcessInviteParamsDto,
13+
ProcessInviteResponseDto,
14+
RoomVersionDto,
15+
} from '../../dtos';
1016

1117
export const invitePlugin = (app: Elysia) => {
1218
const inviteService = container.resolve(InviteService);
1319
const eventAuthService = container.resolve(EventAuthorizationService);
1420

1521
return app.use(isAuthenticatedMiddleware(eventAuthService)).put(
1622
'/_matrix/federation/v2/invite/:roomId/:eventId',
17-
async ({ body, params: { roomId, eventId }, authenticatedServer }) => {
23+
async ({ body, set, params: { roomId, eventId }, authenticatedServer }) => {
1824
if (!authenticatedServer) {
1925
throw new Error('Missing authenticated server from request');
2026
}
2127

22-
return inviteService.processInvite(
23-
body.event,
24-
roomId as RoomID,
25-
eventId as EventID,
26-
body.room_version,
27-
authenticatedServer,
28-
);
28+
try {
29+
return await inviteService.processInvite(
30+
body.event,
31+
roomId as RoomID,
32+
eventId as EventID,
33+
body.room_version,
34+
authenticatedServer,
35+
);
36+
} catch (error) {
37+
if (error instanceof NotAllowedError) {
38+
set.status = 403;
39+
return {
40+
errcode: 'M_FORBIDDEN',
41+
error:
42+
'This server does not allow joining this type of room based on federation settings.',
43+
};
44+
}
45+
46+
set.status = 500;
47+
return {
48+
errcode: 'M_UNKNOWN',
49+
error:
50+
error instanceof Error
51+
? error.message
52+
: 'Internal server error while processing request',
53+
};
54+
}
2955
},
3056
{
3157
params: ProcessInviteParamsDto,
@@ -34,6 +60,11 @@ export const invitePlugin = (app: Elysia) => {
3460
room_version: RoomVersionDto,
3561
invite_room_state: t.Any(),
3662
}),
63+
response: {
64+
200: ProcessInviteResponseDto,
65+
403: FederationErrorResponseDto,
66+
500: FederationErrorResponseDto,
67+
},
3768
detail: {
3869
tags: ['Federation'],
3970
summary: 'Process room invite',
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { type Static, t } from 'elysia';
2+
3+
export const FederationErrorResponseDto = t.Object({
4+
errcode: t.Enum({
5+
M_UNRECOGNIZED: 'M_UNRECOGNIZED',
6+
M_UNAUTHORIZED: 'M_UNAUTHORIZED',
7+
M_FORBIDDEN: 'M_FORBIDDEN',
8+
M_UNKNOWN: 'M_UNKNOWN',
9+
}),
10+
error: t.String(),
11+
});
12+
13+
export type FederationErrorResponseDto = Static<
14+
typeof FederationErrorResponseDto
15+
>;

packages/homeserver/src/dtos/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export * from './federation/state-ids.dto';
1212
export * from './federation/state.dto';
1313
export * from './federation/transactions.dto';
1414
export * from './federation/versions.dto';
15+
export * from './federation/error.dto';
1516

1617
// Internal DTOs
1718
export * from './internal/invite.dto';

packages/homeserver/src/homeserver.module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ export async function setup(options?: HomeserverSetupOptions) {
8888
),
8989
},
9090
},
91+
invite: {
92+
allowedEncryptedRooms:
93+
process.env.INVITE_ALLOWED_ENCRYPTED_ROOMS === 'true',
94+
allowedNonPrivateRooms:
95+
process.env.INVITE_ALLOWED_NON_PRIVATE_ROOMS === 'true',
96+
},
9197
});
9298

9399
const containerOptions: FederationContainerOptions = {

packages/room/src/types/v3-11.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,13 @@ const EncryptedContentSchema = BaseTimelineContentSchema.extend({
537537
.optional(),
538538
});
539539

540+
export const PduEncryptionEventContentSchema = z.object({
541+
algorithm: z
542+
.enum(['m.megolm.v1.aes-sha2'])
543+
.describe('The algorithm used to encrypt the content.'),
544+
ciphertext: z.string().describe('The encrypted content.'),
545+
});
546+
540547
export type PduMessageEventContent = z.infer<
541548
typeof PduMessageEventContentSchema
542549
>;
@@ -706,6 +713,12 @@ const EventPduTypeRoomEncrypted = z.object({
706713
content: EncryptedContentSchema,
707714
});
708715

716+
const EventPduTypeRoomEncryption = z.object({
717+
...PduNoContentEmptyStateKeyStateEventSchema,
718+
type: z.literal('m.room.encryption'),
719+
content: PduEncryptionEventContentSchema,
720+
});
721+
709722
const EventPduTypeRoomMessage = z.object({
710723
...PduNoContentTimelineEventSchema,
711724
type: z.literal('m.room.message'),
@@ -749,6 +762,8 @@ export const PduStateEventSchema = z.discriminatedUnion('type', [
749762
EventPduTypeRoomServerAcl,
750763

751764
EventPduTypeRoomTombstone,
765+
766+
EventPduTypeRoomEncryption,
752767
]);
753768

754769
export const PduTimelineSchema = z.discriminatedUnion('type', [

0 commit comments

Comments
 (0)