Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: verify federation configuration before processing any events #32535

Merged
merged 59 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
1547af1
trying existing method though I am not sure
debdutdeb Jun 1, 2024
3de84a6
maybe
debdutdeb Jun 1, 2024
075a2a2
use right app id
debdutdeb Jun 1, 2024
fe29c6d
add a button
debdutdeb Jun 1, 2024
cf071dc
formatting
debdutdeb Jun 2, 2024
0e830b7
Merge branch 'develop' into federation-verify-config
debdutdeb Jun 30, 2024
acb15a2
new setting for status
debdutdeb Jun 30, 2024
487e1c3
trying something different
debdutdeb Jun 30, 2024
7900fc2
no abstract was weird
debdutdeb Jun 30, 2024
4f907d3
..
debdutdeb Jun 30, 2024
7df9557
...
debdutdeb Jun 30, 2024
d5a1530
hmmm
debdutdeb Jun 30, 2024
a092982
remove stuff
debdutdeb Jun 30, 2024
18050a7
yarn eslint:fix
debdutdeb Jun 30, 2024
bda63bf
smart
debdutdeb Jul 1, 2024
e1e875a
don't forget to propagate change
debdutdeb Jul 1, 2024
77ded52
not sure is the best idea
debdutdeb Jul 1, 2024
ce9cc8c
use service logger
debdutdeb Jul 7, 2024
d48b572
add some deployment files
debdutdeb Jul 7, 2024
af49b1e
update gitignore
debdutdeb Jul 7, 2024
99a4bae
add permission to endpoint
debdutdeb Jul 7, 2024
fc02339
some typescript
debdutdeb Jul 7, 2024
86ae83d
Merge branch 'develop' into federation-verify-config
debdutdeb Jul 7, 2024
3bb58a9
hello typescript
debdutdeb Jul 7, 2024
661efb3
handling txn id from spec
debdutdeb Jul 7, 2024
0c1f3e8
...
debdutdeb Jul 7, 2024
c51ccca
eslint
debdutdeb Jul 7, 2024
fe43b03
add ;
debdutdeb Jul 7, 2024
a9214ff
less lines
debdutdeb Jul 7, 2024
01453a6
hey tests
debdutdeb Jul 7, 2024
5d2a7da
eslint baby
debdutdeb Jul 7, 2024
fe6cc61
i think eslint
debdutdeb Jul 7, 2024
ab00bba
ee tests
debdutdeb Jul 7, 2024
1690ac7
oh my my, how stupid I am
debdutdeb Jul 7, 2024
cd95514
urgh
debdutdeb Jul 8, 2024
15e878f
...
debdutdeb Jul 15, 2024
4fd2fe0
..
debdutdeb Jul 15, 2024
7430e94
Merge branch 'develop' into federation-verify-config
debdutdeb Jul 17, 2024
89eec09
removing some changes to move them to separate branch
debdutdeb Jul 18, 2024
72d92b6
Update apps/meteor/server/services/federation/infrastructure/rocket-c…
debdutdeb Jul 18, 2024
07f3220
Merge branch 'develop' into federation-verify-config
debdutdeb Jul 29, 2024
dcee904
Update apps/meteor/app/lib/server/methods/checkFederationConfiguratio…
debdutdeb Jul 31, 2024
ecb5a35
Merge branch 'develop' into federation-verify-config
debdutdeb Aug 6, 2024
d1176d4
types and formatting
debdutdeb Aug 6, 2024
87efe2a
Merge branch 'develop' into federation-verify-config
debdutdeb Aug 7, 2024
4bcc5ae
Merge branch 'develop' into federation-verify-config
debdutdeb Aug 8, 2024
6a92323
Merge branch 'develop' into federation-verify-config
debdutdeb Aug 12, 2024
38774a9
Merge branch 'develop' into federation-verify-config
debdutdeb Aug 19, 2024
5e179da
review typing
debdutdeb Aug 19, 2024
00df2e9
forgot
debdutdeb Aug 19, 2024
798f474
add changesets
debdutdeb Aug 20, 2024
d5086b9
Merge branch 'develop' into federation-verify-config
debdutdeb Aug 20, 2024
12b43e7
chore: block actions in federated rooms if federation configuration i…
debdutdeb Aug 20, 2024
cd00dce
Merge branch 'develop' into federation-verify-config
debdutdeb Aug 20, 2024
a53554f
reduce some lines
debdutdeb Aug 20, 2024
0756085
return
debdutdeb Aug 20, 2024
1fdc2c9
don't allow if even fed is disabled and room is federated
debdutdeb Aug 20, 2024
de16f5d
since edited on github
debdutdeb Aug 20, 2024
72e606d
Merge branch 'develop' into federation-verify-config
debdutdeb Aug 21, 2024
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,6 @@ yarn-error.log*
*.sublime-workspace

**/.vim/

data/
registration.yaml
18 changes: 18 additions & 0 deletions apps/meteor/app/api/server/v1/federation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,21 @@ API.v1.addRoute(
},
},
);

API.v1.addRoute(
'federation/configuration.verify',
debdutdeb marked this conversation as resolved.
Show resolved Hide resolved
{ authRequired: true, permissionsRequired: ['view-privileged-setting'] },
{
async get() {
const service = License.hasValidLicense() ? FederationEE : Federation;

const status = await service.configurationStatus();

if (!status.externalReachability.ok || !status.appservice.ok) {
return API.v1.failure(status);
}

return API.v1.success(status);
},
},
);
1 change: 1 addition & 0 deletions apps/meteor/app/lib/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,6 @@ import './methods/unarchiveRoom';
import './methods/unblockUser';
import './methods/updateMessage';
import './methods/saveCustomFields';
import './methods/checkFederationConfiguration';

export * from './lib';
80 changes: 80 additions & 0 deletions apps/meteor/app/lib/server/methods/checkFederationConfiguration.ts
debdutdeb marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Federation, FederationEE, Authorization } from '@rocket.chat/core-services';
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { License } from '@rocket.chat/license';
import { Meteor } from 'meteor/meteor';

declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
checkFederationConfiguration(): Promise<{ message: string }>;
}
}

Meteor.methods<ServerMethods>({
async checkFederationConfiguration() {
const uid = Meteor.userId();

if (!uid) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'checkFederationConfiguration',
});
}

if (!(await Authorization.hasPermission(uid, 'view-privileged-setting'))) {
throw new Meteor.Error('error-not-allowed', 'Action not allowed', {
method: 'checkFederationConfiguration',
});
}

const errors: string[] = [];

const successes: string[] = [];

const service = License.hasValidLicense() ? FederationEE : Federation;

const status = await service.configurationStatus();

if (status.externalReachability.ok) {
successes.push('homeserver configuration looks good');
debdutdeb marked this conversation as resolved.
Show resolved Hide resolved
} else {
let err = 'external reachability could not be verified';

const { error } = status.externalReachability;
if (error) {
err += `, error: ${error}`;
}

errors.push(err);
}

const {
roundTrip: { durationMs: duration },
} = status.appservice;

if (status.appservice.ok) {
successes.push(`appservice configuration looks good, total round trip time to homeserver ${duration}ms`);
} else {
errors.push(`failed to verify appservice configuration: ${status.appservice.error}`);
}

if (errors.length) {
void service.markConfigurationInvalid();

if (successes.length) {
const message = ['Configuration could only be partially verified'].concat(successes).concat(errors).join(', ');

throw new Meteor.Error('error-invalid-configuration', message, { method: 'checkFederationConfiguration' });
}

throw new Meteor.Error('error-invalid-configuration', ['Invalid configuration'].concat(errors).join(', '), {
method: 'checkFederationConfiguration',
});
}

void service.markConfigurationValid();

return {
message: ['All configuration looks good'].concat(successes).join(', '),
};
},
});
16 changes: 16 additions & 0 deletions apps/meteor/ee/server/local-services/federation/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,20 @@ export class FederationServiceEE extends AbstractBaseFederationServiceEE impleme
async stopped(): Promise<void> {
return super.stopped();
}

public async verifyConfiguration() {
return super.verifyConfiguration();
}

public async markConfigurationValid() {
return super.markConfigurationValid();
}

public async markConfigurationInvalid() {
return super.markConfigurationInvalid();
}

public async configurationStatus() {
return super.configurationStatus();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,5 @@ export interface IFederationBridge {
externalUserId: string,
externalRoomId: string,
): Promise<{ creator: { id: string; username: string }; name: string; joinedMembers: string[] } | undefined>;
ping(): Promise<{ duration_ms: number }>;
debdutdeb marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ let MatrixUserInstance: any;

const DEFAULT_TIMEOUT_IN_MS_FOR_JOINING_ROOMS = 180000;

const DEFAULT_TIMEOUT_IN_MS_FOR_PING_EVENT = 60 * 1000;

export class MatrixBridge implements IFederationBridge {
protected bridgeInstance: Bridge;

Expand All @@ -44,6 +46,32 @@ export class MatrixBridge implements IFederationBridge {

if (!this.isRunning) {
await this.bridgeInstance.run(this.internalSettings.getBridgePort());

this.bridgeInstance.addAppServicePath({
method: 'POST',
path: '/_matrix/app/v1/ping',
checkToken: true,
handler: (_req, res, _next) => {
/*
* https://spec.matrix.org/v1.11/application-service-api/#post_matrixappv1ping
* Spec does not talk about what to do with the id. It is safe to ignore it as we are already checking for
* homeserver token to be correct.
* From the spec this might be a bit confusing, as it shows a txn id for post, but app service doing nothing with it afterwards
* when receiving from the homeserver.
* From spec directly -
AS ---> HS : /_matrix/client/v1/appservice/{appserviceId}/ping {"transaction_id": "meow"}
HS ---> AS : /_matrix/app/v1/ping {"transaction_id": "meow"}
HS <--- AS : 200 OK {}
AS <--- HS : 200 OK {"duration_ms": 123}
* https://github.com/matrix-org/matrix-spec/blob/e53e6ea8764b95f0bdb738549fca6f9f3f901298/content/application-service-api.md?plain=1#L229-L232
* Code - wise, also doesn't care what happens with the response.
* https://github.com/element-hq/synapse/blob/cb6f4a84a6a8f2b79b80851f37eb5fa4c7c5264a/synapse/rest/client/appservice_ping.py#L80 - nothing done on return
* https://github.com/element-hq/synapse/blob/cb6f4a84a6a8f2b79b80851f37eb5fa4c7c5264a/synapse/appservice/api.py#L321-L332 - not even returning the response, caring for just the http status code - https://github.com/element-hq/synapse/blob/cb6f4a84a6a8f2b79b80851f37eb5fa4c7c5264a/synapse/http/client.py#L532-L537
*/
res.status(200).json({});
},
});

this.isRunning = true;
}
} catch (err) {
Expand Down Expand Up @@ -657,6 +685,10 @@ export class MatrixBridge implements IFederationBridge {
return MatrixEnumSendMessageType.FILE;
}

private getMyHomeServerOrigin() {
return new URL(`https://${this.internalSettings.getHomeServerDomain()}`).hostname;
}

public async uploadContent(
externalSenderId: string,
content: Buffer,
Expand Down Expand Up @@ -724,6 +756,18 @@ export class MatrixBridge implements IFederationBridge {
controller: {
onEvent: (request) => {
const event = request.getData() as unknown as AbstractMatrixEvent;

console.log('event', event);
debdutdeb marked this conversation as resolved.
Show resolved Hide resolved

// TODO(debdut): can we ignore all events from out homeserver?
debdutdeb marked this conversation as resolved.
Show resolved Hide resolved
// This was added particularly to avoid duplicating messages.
// Messages sent from rocket.chat also causes a m.room.message event, which if gets to this bridge
// before the event id promise is resolved, the respective message does not get event id attached to them any longer,
// thus this event handler "resends" the message to the rocket.chat room (not to matrix though).
if (event.type === 'm.room.message' && this.extractHomeserverOrigin(event.sender) === this.getMyHomeServerOrigin()) {
return;
}

this.eventHandler(event);
},
onLog: (line, isError) => {
Expand Down Expand Up @@ -752,4 +796,22 @@ export class MatrixBridge implements IFederationBridge {
'de.sorunome.msc2409.push_ephemeral': registrationFile.enableEphemeralEvents,
};
}

public async ping(): Promise<{ duration_ms: number }> {
debdutdeb marked this conversation as resolved.
Show resolved Hide resolved
if (!this.isRunning || !this.bridgeInstance) {
throw new Error("matrix bridge isn't yet running");
}

return this.bridgeInstance.getIntent().matrixClient.doRequest(
'POST',
`/_matrix/client/v1/appservice/${this.internalSettings.getApplicationServiceId()}/ping`,
{},
/*
* Empty txn id as it is optional, neither does the spec says exactly what to do with it.
* https://github.com/matrix-org/matrix-spec/blob/1fc8f8856fe47849f90344cfa91601c984627acb/data/api/client-server/appservice_ping.yaml#L55-L56
*/
{},
DEFAULT_TIMEOUT_IN_MS_FOR_PING_EVENT,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ export class RocketChatSettingsAdapter {
return settings.get('Federation_Matrix_enable_ephemeral_events') === true;
}

public isConfigurationValid(): boolean {
return settings.get('Federation_Matrix_configuration_status') === 'Valid';
}

public async setConfigurationStatus(status: 'Valid' | 'Invalid'): Promise<void> {
const { modifiedCount } = await Settings.updateOne({ _id: 'Federation_Matrix_configuration_status' }, { $set: { value: status } });
if (modifiedCount) {
void notifyOnSettingChangedById('Federation_Matrix_configuration_status');
}
}

public onFederationEnabledStatusChanged(
callback: (
enabled: boolean,
Expand Down Expand Up @@ -205,7 +216,7 @@ export class RocketChatSettingsAdapter {
const siteUrl = settings.get<string>('Site_Url');

await settingsRegistry.add('Federation_Matrix_id', `rocketchat_${uniqueId}`, {
readonly: true,
readonly: process.env.NODE_ENV === 'production',
type: 'string',
i18nLabel: 'Federation_Matrix_id',
i18nDescription: 'Federation_Matrix_id_desc',
Expand All @@ -214,7 +225,7 @@ export class RocketChatSettingsAdapter {
});

await settingsRegistry.add('Federation_Matrix_hs_token', homeserverToken, {
readonly: true,
readonly: process.env.NODE_ENV === 'production',
type: 'string',
i18nLabel: 'Federation_Matrix_hs_token',
i18nDescription: 'Federation_Matrix_hs_token_desc',
Expand All @@ -223,7 +234,7 @@ export class RocketChatSettingsAdapter {
});

await settingsRegistry.add('Federation_Matrix_as_token', applicationServiceToken, {
readonly: true,
readonly: process.env.NODE_ENV === 'production',
type: 'string',
i18nLabel: 'Federation_Matrix_as_token',
i18nDescription: 'Federation_Matrix_as_token_desc',
Expand Down Expand Up @@ -287,5 +298,27 @@ export class RocketChatSettingsAdapter {
group: 'Federation',
section: 'Matrix Bridge',
});

await settingsRegistry.add('Federation_Matrix_configuration_status', 'Invalid', {
readonly: true,
type: 'string',
i18nLabel: 'Federation_Matrix_configuration_status',
i18nDescription: 'Federation_Matrix_configuration_status_desc',
public: false,
enterprise: false,
invalidValue: '',
group: 'Federation',
section: 'Matrix Bridge',
});

await settingsRegistry.add('Federation_Matrix_check_configuration_button', 'checkFederationConfiguration', {
type: 'action',
actionText: 'Federation_Matrix_check_configuration',
public: false,
enterprise: false,
invalidValue: '',
group: 'Federation',
section: 'Matrix Bridge',
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ import { Logger } from '@rocket.chat/logger';
const logger = new Logger('Federation_Matrix');

export const federationBridgeLogger = logger.section('matrix_federation_bridge');

export const federationServiceLogger = logger.section('matrix_federation_service');
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ async function returnMatrixClientJSON(_: IncomingMessage, res: ServerResponse) {

res.setHeader('content-type', 'application/json');

res.write(JSON.stringify({ 'm.homeserver': `${protocol}//${hostname}` }));
res.write(JSON.stringify({ 'm.homeserver': { base_url: `${protocol}//${hostname}` } }));

res.end();
}
Expand Down
Loading
Loading