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 5 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
68 changes: 68 additions & 0 deletions apps/meteor/app/api/server/v1/federation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
import { Federation, FederationEE } from '@rocket.chat/core-services';
import { License } from '@rocket.chat/license';
import { isFederationVerifyMatrixIdProps } from '@rocket.chat/rest-typings';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';

Check failure on line 4 in apps/meteor/app/api/server/v1/federation.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

There should be at least one empty line between import groups
import { parse as urlParse } from 'node:url';

Check failure on line 5 in apps/meteor/app/api/server/v1/federation.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

`node:url` import should occur before import of `@rocket.chat/core-services`

import { API } from '../api';
import { settings } from '../../../settings/server';

Check failure on line 8 in apps/meteor/app/api/server/v1/federation.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

`../../../settings/server` import should occur before import of `../api`

const federationTesterHost = process.env.FEDERATION_TESTER_HOST?.trim()?.replace(/\/$/, '') || 'https://federationtester.matrix.org';

function checkFederation(): Promise<boolean> {
const url = urlParse(settings.get('Site_Url'));
debdutdeb marked this conversation as resolved.
Show resolved Hide resolved

let domain = url.hostname;

if (url.port) {
domain += ':' + url.port;

Check failure on line 18 in apps/meteor/app/api/server/v1/federation.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

Unexpected string concatenation
}

return new Promise((resolve, reject) =>
fetch(`${federationTesterHost}/api/federation-ok?server_name=${domain}`)
.then((response) => response.text())
.then((text) => resolve(text === 'GOOD'))
.catch(reject),
);
}

API.v1.addRoute(
'federation/matrixIds.verify',
Expand All @@ -22,3 +44,49 @@
},
},
);

API.v1.addRoute(
'federation.verifyConfiguration',
{ authRequired: false },
debdutdeb marked this conversation as resolved.
Show resolved Hide resolved
{
async get() {
const federationService = License.hasValidLicense() ? FederationEE : Federation;

const response = {
appservice: { duration_ms: -1, ok: false },
debdutdeb marked this conversation as resolved.
Show resolved Hide resolved
federation: {
ok: false,
},
};

try {
response.federation.ok = await checkFederation();
} catch (error) {
return API.v1.failure({
federation: {
ok: false,
error: String(error),
},
});
}

try {
response.appservice = await federationService.verifyConfiguration();
response.appservice.ok = true;
} catch (error) {
return API.v1.failure({
federation: response.federation,
appservice: {
error: String(error),
},
});
}

if (response.federation.ok) {
return API.v1.success(response);
} else {

Check failure on line 87 in apps/meteor/app/api/server/v1/federation.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

Unnecessary 'else' after 'return'
return API.v1.failure(response);
}
},
},
);
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';
93 changes: 93 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,93 @@
import { Federation, FederationEE } from '@rocket.chat/core-services';

Check failure on line 1 in apps/meteor/app/lib/server/methods/checkFederationConfiguration.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

'/home/runner/work/Rocket.Chat/Rocket.Chat/apps/meteor/node_modules/@rocket.chat/core-services/dist/index.js' imported multiple times
import { License } from '@rocket.chat/license';
import type { ServerMethods } from '@rocket.chat/ui-contexts';
import { Meteor } from 'meteor/meteor';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';

Check failure on line 5 in apps/meteor/app/lib/server/methods/checkFederationConfiguration.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

There should be at least one empty line between import groups

Check failure on line 5 in apps/meteor/app/lib/server/methods/checkFederationConfiguration.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

`@rocket.chat/server-fetch` import should occur before import of `@rocket.chat/ui-contexts`
import { parse as urlParse } from 'node:url';

Check failure on line 6 in apps/meteor/app/lib/server/methods/checkFederationConfiguration.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

There should be at least one empty line between import groups
import { Authorization } from '@rocket.chat/core-services';

import { settings } from '../../../settings/server';

const federationTesterHost = process.env.FEDERATION_TESTER_HOST?.trim()?.replace(/\/$/, '') || 'https://federationtester.matrix.org';

function checkFederation(): Promise<boolean> {
const url = urlParse(settings.get('Site_Url'));

let domain = url.hostname;

if (url.port) {
domain += ':' + url.port;
}

return new Promise((resolve, reject) =>
fetch(`${federationTesterHost}/api/federation-ok?server_name=${domain}`)
.then((response) => response.text())
.then((text) => resolve(text === 'GOOD'))
.catch(reject),
);
}

declare module '@rocket.chat/ui-contexts' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
checkFederationConfiguration(): {};
}
}

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;

try {
if (!(await checkFederation())) {
errors.push('external reachability could not be verified');
// throw new Meteor.Error('error-invalid-configuration',, { method: 'checkFederationConfiguration' });
} else {
successes.push('homeserver configuration looks good');
}
} catch (error) {
errors.push(`failed to verify external reachability: ${String(error)}`);
}

try {
const { duration_ms: duration } = await service.verifyConfiguration();
successes.push(`appservice configuration looks good, total round trip time to homeserver ${duration}ms`);
} catch (error) {
errors.push(`failed to verify appservice configuration: ${String(error)}`);
}

if (errors.length) {
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',
});
}

return {
message: ['All configuration looks good'].concat(successes).join(', '),
};
},
});
4 changes: 4 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,8 @@ export class FederationServiceEE extends AbstractBaseFederationServiceEE impleme
async stopped(): Promise<void> {
return super.stopped();
}

public async verifyConfiguration() {
return super.verifyConfiguration()
}
}
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
@@ -1,6 +1,7 @@
import type { IMessage } from '@rocket.chat/core-typings';
import type { AppServiceOutput, Bridge } from '@rocket.chat/forked-matrix-appservice-bridge';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
import { type Request, type Response } from 'express';

import type { IExternalUserProfileInformation, IFederationBridge, IFederationBridgeRegistrationFile } from '../../domain/IFederationBridge';
import type { RocketChatSettingsAdapter } from '../rocket-chat/adapters/Settings';
Expand Down Expand Up @@ -43,7 +44,25 @@ export class MatrixBridge implements IFederationBridge {
await this.createInstance();

if (!this.isRunning) {
await this.bridgeInstance.run(this.internalSettings.getBridgePort());
const { AppService } = await import('@rocket.chat/forked-matrix-appservice-bridge');

const appservice = new AppService({ homeserverToken: this.internalSettings.getAppServiceRegistrationObject().homeserverToken });

const pinghandler = function (req: Request, res: Response) {
if (req.headers.authorization?.split(/\s+/)[1] !== (this as any).config.homeserverToken) {
res.status(401);
res.end();
return;
}

res.status(200);
res.json(req.body);
};

appservice.expressApp.post('/_matrix/app/v1/ping', pinghandler.bind(appservice));

await this.bridgeInstance.run(this.internalSettings.getBridgePort(), appservice);

this.isRunning = true;
}
} catch (err) {
Expand Down Expand Up @@ -752,4 +771,20 @@ 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`,
{},
{ transaction_id: 'meow' },
DEFAULT_TIMEOUT_IN_MS_FOR_JOINING_ROOMS,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -285,5 +285,15 @@ export class RocketChatSettingsAdapter {
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',
});
}
}
8 changes: 8 additions & 0 deletions apps/meteor/server/services/federation/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@ export abstract class AbstractFederationService extends ServiceClassInternal {
protected async verifyMatrixIds(matrixIds: string[]): Promise<Map<string, string>> {
return this.bridge.verifyInviteeIds(matrixIds);
}

public async verifyConfiguration() {
debdutdeb marked this conversation as resolved.
Show resolved Hide resolved
return this.bridge.ping()
}
}

abstract class AbstractBaseFederationService extends AbstractFederationService {
Expand Down Expand Up @@ -342,4 +346,8 @@ export class FederationService extends AbstractBaseFederationService implements
public async created(): Promise<void> {
return super.created();
}

public async verifyConfiguration() {
return super.verifyConfiguration()
}
}
4 changes: 4 additions & 0 deletions packages/core-services/src/types/IFederationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export interface IFederationService {
createDirectMessageRoomAndInviteUser(internalInviterId: string, internalRoomId: string, externalInviteeId: string): Promise<void>;

verifyMatrixIds(matrixIds: string[]): Promise<Map<string, string>>;

verifyConfiguration(): Promise<{ duration_ms: number }>;
debdutdeb marked this conversation as resolved.
Show resolved Hide resolved
}

export interface IFederationJoinExternalPublicRoomInput {
Expand Down Expand Up @@ -38,4 +40,6 @@ export interface IFederationServiceEE {
joinExternalPublicRoom(input: IFederationJoinExternalPublicRoomInput): Promise<void>;

verifyMatrixIds(matrixIds: string[]): Promise<Map<string, string>>;

verifyConfiguration(): Promise<{ duration_ms: number }>;
debdutdeb marked this conversation as resolved.
Show resolved Hide resolved
}
1 change: 1 addition & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -2320,6 +2320,7 @@
"Federation_Matrix_serve_well_known": "Serve Well Known",
"Federation_Matrix_serve_well_known_Description": "Serve /.well-known/matrix/server and /.well-known/matrix/client directly from within Rocket.Chat instead of reverse proxy for federation",
"Federation_Matrix_serve_well_known_Alert": "Keep this off if using DNS srv records for federation, or use a reverse proxy to return static JSON if federation traffic is heavy. <a target=\"_blank\" href=\"https://matrix-org.github.io/synapse/latest/federate.html\">Read mode</a>.",
"Federation_Matrix_check_configuration": "Verify configuration",
"Field": "Field",
"Field_removed": "Field removed",
"Field_required": "Field required",
Expand Down
Loading