Skip to content

Commit ea1bdb6

Browse files
authored
chore: Expose workspace hashed url in workspace admin page and startup logs (#40685)
1 parent bd399c5 commit ea1bdb6

8 files changed

Lines changed: 104 additions & 10 deletions

File tree

apps/meteor/app/api/server/lib/getServerInfo.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { IWorkspaceInfo } from '@rocket.chat/core-typings';
2+
import { License } from '@rocket.chat/license';
23

34
import { getTrimmedServerVersion } from './getTrimmedServerVersion';
45
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
@@ -15,6 +16,8 @@ export async function getServerInfo(userId?: string): Promise<IWorkspaceInfo> {
1516
const cloudWorkspaceId = settings.get<string | undefined>('Cloud_Workspace_Id');
1617

1718
return {
19+
workspaceUrl: License.getWorkspaceUrl(),
20+
hashedWorkspaceUrl: License.getHashedWorkspaceUrl(),
1821
version: getTrimmedServerVersion(),
1922
...(hasPermissionToViewStatistics && {
2023
info: {

apps/meteor/client/views/admin/workspace/DeploymentCard/DeploymentCard.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ type DeploymentCardProps = {
1717
statistics: IStats;
1818
};
1919

20-
const DeploymentCard = ({ serverInfo: { info, cloudWorkspaceId }, statistics, instances }: DeploymentCardProps) => {
20+
const DeploymentCard = ({
21+
serverInfo: { info, cloudWorkspaceId, workspaceUrl, hashedWorkspaceUrl },
22+
statistics,
23+
instances,
24+
}: DeploymentCardProps) => {
2125
const { t } = useTranslation();
2226
const formatDateAndTime = useFormatDateAndTime();
2327
const setModal = useSetModal();
@@ -39,6 +43,18 @@ const DeploymentCard = ({ serverInfo: { info, cloudWorkspaceId }, statistics, in
3943
<WorkspaceCardSectionTitle title={t('Version')} />
4044
{statistics.version}
4145
</WorkspaceCardSection>
46+
{workspaceUrl && (
47+
<WorkspaceCardSection>
48+
<WorkspaceCardSectionTitle title={t('Site_Url')} />
49+
{workspaceUrl}
50+
</WorkspaceCardSection>
51+
)}
52+
{hashedWorkspaceUrl && (
53+
<WorkspaceCardSection>
54+
<WorkspaceCardSectionTitle title={t('Hashed_Site_Url')} />
55+
<span style={{ lineBreak: 'anywhere' }}>{hashedWorkspaceUrl}</span>
56+
</WorkspaceCardSection>
57+
)}
4258
<WorkspaceCardSection>
4359
<WorkspaceCardSectionTitle title={t('Deployment_ID')} />
4460
{statistics.uniqueId}

apps/meteor/server/startup/serverRunning.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from 'node:fs';
22
import path from 'node:path';
33

4+
import { License } from '@rocket.chat/license';
45
// import { Users } from '@rocket.chat/models';
56
import { Meteor } from 'meteor/meteor';
67
import semver from 'semver';
@@ -48,7 +49,8 @@ Meteor.startup(async () => {
4849
` MongoDB Engine: ${mongoStorageEngine}`,
4950
` Platform: ${process.platform}`,
5051
` Process Port: ${process.env.PORT}`,
51-
` Site URL: ${settings.get('Site_Url')}`,
52+
` Site URL: ${settings.get<string>('Site_Url')}`,
53+
` Hashed Site URL: ${License.getHashedWorkspaceUrl()}`,
5254
];
5355

5456
if (Info.commit?.hash) {

ee/packages/license/src/license.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import crypto from 'node:crypto';
2+
13
import type {
24
ILicenseTag,
35
LicenseEvents,
@@ -161,6 +163,20 @@ export abstract class LicenseManager extends Emitter<LicenseEvents> {
161163
return this.workspaceUrl;
162164
}
163165

166+
public hashWorkspaceUrl(url: string) {
167+
return crypto.createHash('sha256').update(url).digest('hex');
168+
}
169+
170+
public getHashedWorkspaceUrl() {
171+
const workspaceUrl = this.getWorkspaceUrl();
172+
173+
if (!workspaceUrl) {
174+
return undefined;
175+
}
176+
177+
return this.hashWorkspaceUrl(workspaceUrl);
178+
}
179+
164180
public async revalidateLicense(options: Omit<LicenseValidationOptions, 'isNewLicense'> = {}): Promise<void> {
165181
if (!this.hasValidLicense()) {
166182
return;

ee/packages/license/src/validation/validateLicenseUrl.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,37 @@ describe('Url Validation', () => {
127127
).toStrictEqual([]);
128128
});
129129
});
130+
131+
describe('type mismatch', () => {
132+
it('should validate as hash if the type is url but the value looks like a hash', async () => {
133+
const licenseManager = await getReadyLicenseManager();
134+
135+
const hash = crypto.createHash('sha256').update('localhost:3000').digest('hex');
136+
const license = await new MockedLicenseBuilder().withServerUrls({
137+
value: hash,
138+
type: 'url',
139+
});
140+
await expect(
141+
validateLicenseUrl.call(licenseManager, await license.build(), {
142+
behaviors: ['invalidate_license', 'prevent_installation', 'start_fair_policy', 'disable_modules'],
143+
suppressLog: false,
144+
}),
145+
).toStrictEqual([]);
146+
});
147+
148+
it('should validate as url if the type is hash but the value looks like a url', async () => {
149+
const licenseManager = await getReadyLicenseManager();
150+
151+
const license = await new MockedLicenseBuilder().withServerUrls({
152+
value: 'localhost:3000',
153+
type: 'hash',
154+
});
155+
await expect(
156+
validateLicenseUrl.call(licenseManager, await license.build(), {
157+
behaviors: ['invalidate_license', 'prevent_installation', 'start_fair_policy', 'disable_modules'],
158+
suppressLog: false,
159+
}),
160+
).toStrictEqual([]);
161+
});
162+
});
130163
});

ee/packages/license/src/validation/validateLicenseUrl.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import crypto from 'node:crypto';
2-
31
import type { ILicenseV3, BehaviorWithContext, LicenseValidationOptions } from '@rocket.chat/core-typings';
42

53
import { isBehaviorAllowed } from '../isItemAllowed';
@@ -20,9 +18,8 @@ const validateUrl = (licenseURL: string, url: string) => {
2018
return licenseURL.toLowerCase() === url.toLowerCase();
2119
};
2220

23-
const validateHash = (licenseURL: string, url: string) => {
24-
const value = crypto.createHash('sha256').update(url).digest('hex');
25-
return licenseURL === value;
21+
const validateHash = (licenseURL: string, hashedUrl: string) => {
22+
return licenseURL === hashedUrl;
2623
};
2724

2825
export function validateLicenseUrl(this: LicenseManager, license: ILicenseV3, options: LicenseValidationOptions): BehaviorWithContext[] {
@@ -41,25 +38,49 @@ export function validateLicenseUrl(this: LicenseManager, license: ILicenseV3, op
4138
return [getResultingBehavior({ behavior: 'invalidate_license' }, { reason: 'url' })];
4239
}
4340

41+
const hashedWorkspaceUrl = this.hashWorkspaceUrl(workspaceUrl);
42+
4443
return serverUrls
4544
.filter((url) => {
45+
if (
46+
url.type === 'url' &&
47+
url.value.length === 64 &&
48+
/^[a-f0-9]{64}$/i.test(url.value) &&
49+
validateHash(url.value, hashedWorkspaceUrl)
50+
) {
51+
// If the url type is 'url' but the value looks like a hash, validate it as a hash to avoid invalidating licenses unnecessarily.
52+
logger.warn(
53+
`License URL with type 'url' is actually a hash. Validating as hash to avoid invalidating license unnecessarily. url: ${url.value}`,
54+
);
55+
return false;
56+
}
57+
58+
if (url.type === 'hash' && !/^[a-f0-9]{64}$/i.test(url.value) && validateUrl(url.value, workspaceUrl)) {
59+
// If the url type is 'hash' but the value looks like a url, validate it as a url to avoid invalidating licenses unnecessarily.
60+
logger.warn(
61+
`License URL with type 'hash' does not look like a hash. Validating as url to avoid invalidating license unnecessarily. url: ${url.value}`,
62+
);
63+
return false;
64+
}
65+
4666
switch (url.type) {
4767
case 'regex':
4868
return !validateRegex(url.value, workspaceUrl);
4969
case 'hash':
50-
return !validateHash(url.value, workspaceUrl);
70+
return !validateHash(url.value, hashedWorkspaceUrl);
5171
case 'url':
5272
return !validateUrl(url.value, workspaceUrl);
5373
default:
54-
return false;
74+
return true; // If the type is unknown, consider it invalid to be safe.
5575
}
5676
})
5777
.map((url) => {
5878
if (!options.suppressLog) {
5979
logger.error({
6080
msg: 'Url validation failed',
61-
url,
81+
licenseUrl: url,
6282
workspaceUrl,
83+
hashedWorkspaceUrl,
6384
});
6485
}
6586
return getResultingBehavior({ behavior: 'invalidate_license' }, { reason: 'url' });

packages/core-typings/src/IWorkspaceInfo.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { IServerInfo } from './IServerInfo';
22

33
export interface IWorkspaceInfo {
4+
workspaceUrl?: string;
5+
hashedWorkspaceUrl?: string;
46
info?: IServerInfo;
57
supportedVersions?: { signed: string };
68
minimumClientVersions: { desktop: string; mobile: string };

packages/i18n/src/locales/en.i18n.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2538,6 +2538,7 @@
25382538
"HTML": "HTML",
25392539
"Hang_up_and_transfer_call": "Hang up and transfer call",
25402540
"Hash": "Hash",
2541+
"Hashed_Site_Url": "Hashed Site URL",
25412542
"Header": "Header",
25422543
"Header_and_Footer": "Header and Footer",
25432544
"Healthcare": "Healthcare",

0 commit comments

Comments
 (0)