diff --git a/docs/content/config/index.md b/docs/content/config/index.md index 292bc8186..93c98c097 100644 --- a/docs/content/config/index.md +++ b/docs/content/config/index.md @@ -23,7 +23,7 @@ We also provide an `.env.example` file containing a minimal configuration in the |--------------------------|-----------|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| | `HD_DOMAIN` | - | `https://md.example.com` | The URL the HedgeDoc instance runs on. | | `PORT` | 3000 | | The port the HedgeDoc instance runs on. | -| `HD_RENDERER_ORIGIN` | HD_DOMAIN | | The URL the renderer runs on. If omitted this will be same as `HD_DOMAIN`. | +| `HD_RENDERER_BASE_URL` | HD_DOMAIN | | The URL the renderer runs on. If omitted this will be same as `HD_DOMAIN`. | | `HD_LOGLEVEL` | warn | | The loglevel that should be used. Options are `error`, `warn`, `info`, `debug` or `trace`. | | `HD_FORBIDDEN_NOTE_IDS` | - | `notAllowed,alsoNotAllowed` | A list of note ids (separated by `,`), that are not allowed to be created or requested by anyone. | | `HD_MAX_DOCUMENT_LENGTH` | 100000 | | The maximum length of any one document. Changes to this will impact performance for your users. | diff --git a/src/app-init.ts b/src/app-init.ts index 44109eade..6c1a90915 100644 --- a/src/app-init.ts +++ b/src/app-init.ts @@ -52,9 +52,12 @@ export async function setupApp( ); app.enableCors({ - origin: appConfig.rendererOrigin, + origin: appConfig.rendererBaseUrl, }); - logger.log(`Enabling CORS for '${appConfig.rendererOrigin}'`, 'AppBootstrap'); + logger.log( + `Enabling CORS for '${appConfig.rendererBaseUrl}'`, + 'AppBootstrap', + ); app.useGlobalPipes(setupValidationPipe(logger)); diff --git a/src/config/app.config.spec.ts b/src/config/app.config.spec.ts index 49dbccc15..37e350d26 100644 --- a/src/config/app.config.spec.ts +++ b/src/config/app.config.spec.ts @@ -11,7 +11,7 @@ import { Loglevel } from './loglevel.enum'; describe('appConfig', () => { const domain = 'https://example.com'; const invalidDomain = 'localhost'; - const rendererOrigin = 'https://render.example.com'; + const rendererBaseUrl = 'https://render.example.com'; const port = 3333; const negativePort = -9000; const floatPort = 3.14; @@ -27,7 +27,7 @@ describe('appConfig', () => { { /* eslint-disable @typescript-eslint/naming-convention */ HD_DOMAIN: domain, - HD_RENDERER_ORIGIN: rendererOrigin, + HD_RENDERER_BASE_URL: rendererBaseUrl, PORT: port.toString(), HD_LOGLEVEL: loglevel, HD_PERSIST_INTERVAL: '100', @@ -39,7 +39,7 @@ describe('appConfig', () => { ); const config = appConfig(); expect(config.domain).toEqual(domain); - expect(config.rendererOrigin).toEqual(rendererOrigin); + expect(config.rendererBaseUrl).toEqual(rendererBaseUrl); expect(config.port).toEqual(port); expect(config.loglevel).toEqual(loglevel); expect(config.persistInterval).toEqual(100); @@ -62,7 +62,7 @@ describe('appConfig', () => { ); const config = appConfig(); expect(config.domain).toEqual(domain); - expect(config.rendererOrigin).toEqual(domain); + expect(config.rendererBaseUrl).toEqual(domain); expect(config.port).toEqual(port); expect(config.loglevel).toEqual(loglevel); expect(config.persistInterval).toEqual(100); @@ -74,7 +74,7 @@ describe('appConfig', () => { { /* eslint-disable @typescript-eslint/naming-convention */ HD_DOMAIN: domain, - HD_RENDERER_ORIGIN: rendererOrigin, + HD_RENDERER_BASE_URL: rendererBaseUrl, HD_LOGLEVEL: loglevel, HD_PERSIST_INTERVAL: '100', /* eslint-enable @typescript-eslint/naming-convention */ @@ -85,7 +85,7 @@ describe('appConfig', () => { ); const config = appConfig(); expect(config.domain).toEqual(domain); - expect(config.rendererOrigin).toEqual(rendererOrigin); + expect(config.rendererBaseUrl).toEqual(rendererBaseUrl); expect(config.port).toEqual(3000); expect(config.loglevel).toEqual(loglevel); expect(config.persistInterval).toEqual(100); @@ -97,7 +97,7 @@ describe('appConfig', () => { { /* eslint-disable @typescript-eslint/naming-convention */ HD_DOMAIN: domain, - HD_RENDERER_ORIGIN: rendererOrigin, + HD_RENDERER_BASE_URL: rendererBaseUrl, PORT: port.toString(), HD_PERSIST_INTERVAL: '100', /* eslint-enable @typescript-eslint/naming-convention */ @@ -108,7 +108,7 @@ describe('appConfig', () => { ); const config = appConfig(); expect(config.domain).toEqual(domain); - expect(config.rendererOrigin).toEqual(rendererOrigin); + expect(config.rendererBaseUrl).toEqual(rendererBaseUrl); expect(config.port).toEqual(port); expect(config.loglevel).toEqual(Loglevel.WARN); expect(config.persistInterval).toEqual(100); @@ -120,7 +120,7 @@ describe('appConfig', () => { { /* eslint-disable @typescript-eslint/naming-convention */ HD_DOMAIN: domain, - HD_RENDERER_ORIGIN: rendererOrigin, + HD_RENDERER_BASE_URL: rendererBaseUrl, HD_LOGLEVEL: loglevel, PORT: port.toString(), /* eslint-enable @typescript-eslint/naming-convention */ @@ -131,7 +131,7 @@ describe('appConfig', () => { ); const config = appConfig(); expect(config.domain).toEqual(domain); - expect(config.rendererOrigin).toEqual(rendererOrigin); + expect(config.rendererBaseUrl).toEqual(rendererBaseUrl); expect(config.port).toEqual(port); expect(config.loglevel).toEqual(Loglevel.TRACE); expect(config.persistInterval).toEqual(10); @@ -143,7 +143,7 @@ describe('appConfig', () => { { /* eslint-disable @typescript-eslint/naming-convention */ HD_DOMAIN: domain, - HD_RENDERER_ORIGIN: rendererOrigin, + HD_RENDERER_BASE_URL: rendererBaseUrl, HD_LOGLEVEL: loglevel, PORT: port.toString(), HD_PERSIST_INTERVAL: '0', @@ -155,7 +155,7 @@ describe('appConfig', () => { ); const config = appConfig(); expect(config.domain).toEqual(domain); - expect(config.rendererOrigin).toEqual(rendererOrigin); + expect(config.rendererBaseUrl).toEqual(rendererBaseUrl); expect(config.port).toEqual(port); expect(config.loglevel).toEqual(Loglevel.TRACE); expect(config.persistInterval).toEqual(0); diff --git a/src/config/app.config.ts b/src/config/app.config.ts index 912e38f17..b032a7265 100644 --- a/src/config/app.config.ts +++ b/src/config/app.config.ts @@ -11,7 +11,7 @@ import { buildErrorMessage, parseOptionalNumber } from './utils'; export interface AppConfig { domain: string; - rendererOrigin: string; + rendererBaseUrl: string; port: number; loglevel: Loglevel; persistInterval: number; @@ -23,13 +23,13 @@ const schema = Joi.object({ scheme: /https?/, }) .label('HD_DOMAIN'), - rendererOrigin: Joi.string() + rendererBaseUrl: Joi.string() .uri({ scheme: /https?/, }) .default(Joi.ref('domain')) .optional() - .label('HD_RENDERER_ORIGIN'), + .label('HD_RENDERER_BASE_URL'), port: Joi.number() .positive() .integer() @@ -54,7 +54,7 @@ export default registerAs('appConfig', () => { const appConfig = schema.validate( { domain: process.env.HD_DOMAIN, - rendererOrigin: process.env.HD_RENDERER_ORIGIN, + rendererBaseUrl: process.env.HD_RENDERER_BASE_URL, port: parseOptionalNumber(process.env.PORT), loglevel: process.env.HD_LOGLEVEL, persistInterval: process.env.HD_PERSIST_INTERVAL, diff --git a/src/config/mock/app.config.mock.ts b/src/config/mock/app.config.mock.ts index e79938911..952d75ff6 100644 --- a/src/config/mock/app.config.mock.ts +++ b/src/config/mock/app.config.mock.ts @@ -12,7 +12,7 @@ export default registerAs( 'appConfig', (): AppConfig => ({ domain: 'md.example.com', - rendererOrigin: 'md-renderer.example.com', + rendererBaseUrl: 'md-renderer.example.com', port: 3000, loglevel: Loglevel.ERROR, persistInterval: 10, diff --git a/src/frontend-config/frontend-config.dto.ts b/src/frontend-config/frontend-config.dto.ts index 458caaa4f..223af14f3 100644 --- a/src/frontend-config/frontend-config.dto.ts +++ b/src/frontend-config/frontend-config.dto.ts @@ -123,22 +123,6 @@ export class SpecialUrlsDto extends BaseDto { imprint?: URL; } -export class IframeCommunicationDto extends BaseDto { - /** - * The origin under which the editor page will be served - * @example https://md.example.com - */ - @IsUrl() - editorOrigin: URL; - - /** - * The origin under which the renderer page will be served - * @example https://md-renderer.example.com - */ - @IsUrl() - rendererOrigin: URL; -} - export class FrontendConfigDto extends BaseDto { /** * Maximum access level for guest users @@ -195,12 +179,4 @@ export class FrontendConfigDto extends BaseDto { */ @IsNumber() maxDocumentLength: number; - - /** - * The frontend capsules the markdown rendering into a secured iframe, to increase the security. The browser will treat the iframe target as cross-origin even if they are on the same domain. - * You can go even one step further and serve the editor and the renderer on different (sub)domains to eliminate even more attack vectors by making sessions, cookies, etc. not available for the renderer, because they aren't set on the renderer origin. - * However, The editor and the renderer need to know the other's origin to communicate with each other, even if they are the same. - */ - @ValidateNested() - iframeCommunication: IframeCommunicationDto; } diff --git a/src/frontend-config/frontend-config.service.spec.ts b/src/frontend-config/frontend-config.service.spec.ts index 8fa9a1bd1..849c0a40d 100644 --- a/src/frontend-config/frontend-config.service.spec.ts +++ b/src/frontend-config/frontend-config.service.spec.ts @@ -168,7 +168,7 @@ describe('FrontendConfigService', () => { it(`works with ${JSON.stringify(authConfigConfigured)}`, async () => { const appConfig: AppConfig = { domain: domain, - rendererOrigin: domain, + rendererBaseUrl: 'https://renderer.example.org', port: 3000, loglevel: Loglevel.ERROR, persistInterval: 10, @@ -315,119 +315,106 @@ describe('FrontendConfigService', () => { const customName = 'Test Branding Name'; let index = 1; - for (const renderOrigin of [undefined, 'http://md-renderer.example.com']) { - for (const customLogo of [undefined, 'https://example.com/logo.png']) { - for (const privacyLink of [undefined, 'https://example.com/privacy']) { - for (const termsOfUseLink of [undefined, 'https://example.com/terms']) { - for (const imprintLink of [ + for (const customLogo of [undefined, 'https://example.com/logo.png']) { + for (const privacyLink of [undefined, 'https://example.com/privacy']) { + for (const termsOfUseLink of [undefined, 'https://example.com/terms']) { + for (const imprintLink of [undefined, 'https://example.com/imprint']) { + for (const plantUmlServer of [ undefined, - 'https://example.com/imprint', + 'https://plantuml.example.com', ]) { - for (const plantUmlServer of [ - undefined, - 'https://plantuml.example.com', - ]) { - it(`combination #${index} works`, async () => { - const appConfig: AppConfig = { - domain: domain, - rendererOrigin: renderOrigin ?? domain, - port: 3000, - loglevel: Loglevel.ERROR, - persistInterval: 10, - }; - const authConfig: AuthConfig = { - ...emptyAuthConfig, - local: { - enableLogin: true, - enableRegister, - minimalPasswordStrength: 3, + it(`combination #${index} works`, async () => { + const appConfig: AppConfig = { + domain: domain, + rendererBaseUrl: 'https://renderer.example.org', + port: 3000, + loglevel: Loglevel.ERROR, + persistInterval: 10, + }; + const authConfig: AuthConfig = { + ...emptyAuthConfig, + local: { + enableLogin: true, + enableRegister, + minimalPasswordStrength: 3, + }, + }; + const customizationConfig: CustomizationConfig = { + branding: { + customName: customName, + customLogo: customLogo, + }, + specialUrls: { + privacy: privacyLink, + termsOfUse: termsOfUseLink, + imprint: imprintLink, + }, + }; + const externalServicesConfig: ExternalServicesConfig = { + plantUmlServer: plantUmlServer, + imageProxy: imageProxy, + }; + const noteConfig: NoteConfig = { + forbiddenNoteIds: [], + maxDocumentLength: maxDocumentLength, + guestAccess: GuestAccess.CREATE, + permissions: { + default: { + everyone: DefaultAccessPermission.READ, + loggedIn: DefaultAccessPermission.WRITE, }, - }; - const customizationConfig: CustomizationConfig = { - branding: { - customName: customName, - customLogo: customLogo, - }, - specialUrls: { - privacy: privacyLink, - termsOfUse: termsOfUseLink, - imprint: imprintLink, - }, - }; - const externalServicesConfig: ExternalServicesConfig = { - plantUmlServer: plantUmlServer, - imageProxy: imageProxy, - }; - const noteConfig: NoteConfig = { - forbiddenNoteIds: [], - maxDocumentLength: maxDocumentLength, - guestAccess: GuestAccess.CREATE, - permissions: { - default: { - everyone: DefaultAccessPermission.READ, - loggedIn: DefaultAccessPermission.WRITE, - }, - }, - }; - const module: TestingModule = await Test.createTestingModule({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - load: [ - registerAs('appConfig', () => appConfig), - registerAs('authConfig', () => authConfig), - registerAs( - 'customizationConfig', - () => customizationConfig, - ), - registerAs( - 'externalServicesConfig', - () => externalServicesConfig, - ), - registerAs('noteConfig', () => noteConfig), - ], - }), - LoggerModule, - ], - providers: [FrontendConfigService], - }).compile(); + }, + }; + const module: TestingModule = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [ + registerAs('appConfig', () => appConfig), + registerAs('authConfig', () => authConfig), + registerAs( + 'customizationConfig', + () => customizationConfig, + ), + registerAs( + 'externalServicesConfig', + () => externalServicesConfig, + ), + registerAs('noteConfig', () => noteConfig), + ], + }), + LoggerModule, + ], + providers: [FrontendConfigService], + }).compile(); - const service = module.get(FrontendConfigService); - const config = await service.getFrontendConfig(); - expect(config.allowRegister).toEqual(enableRegister); - expect(config.guestAccess).toEqual(noteConfig.guestAccess); - expect(config.branding.name).toEqual(customName); - expect(config.branding.logo).toEqual( - customLogo ? new URL(customLogo) : undefined, - ); - expect(config.iframeCommunication.editorOrigin).toEqual( - new URL(appConfig.domain), - ); - expect(config.iframeCommunication.rendererOrigin).toEqual( - appConfig.rendererOrigin - ? new URL(appConfig.rendererOrigin) - : new URL(appConfig.domain), - ); - expect(config.maxDocumentLength).toEqual(maxDocumentLength); - expect(config.plantUmlServer).toEqual( - plantUmlServer ? new URL(plantUmlServer) : undefined, - ); - expect(config.specialUrls.imprint).toEqual( - imprintLink ? new URL(imprintLink) : undefined, - ); - expect(config.specialUrls.privacy).toEqual( - privacyLink ? new URL(privacyLink) : undefined, - ); - expect(config.specialUrls.termsOfUse).toEqual( - termsOfUseLink ? new URL(termsOfUseLink) : undefined, - ); - expect(config.useImageProxy).toEqual(!!imageProxy); - expect(config.version).toEqual( - await getServerVersionFromPackageJson(), - ); - }); - index += 1; - } + const service = module.get(FrontendConfigService); + const config = await service.getFrontendConfig(); + expect(config.allowRegister).toEqual(enableRegister); + expect(config.guestAccess).toEqual(noteConfig.guestAccess); + expect(config.branding.name).toEqual(customName); + expect(config.branding.logo).toEqual( + customLogo ? new URL(customLogo) : undefined, + ); + expect(config.maxDocumentLength).toEqual(maxDocumentLength); + expect(config.plantUmlServer).toEqual( + plantUmlServer ? new URL(plantUmlServer) : undefined, + ); + expect(config.specialUrls.imprint).toEqual( + imprintLink ? new URL(imprintLink) : undefined, + ); + expect(config.specialUrls.privacy).toEqual( + privacyLink ? new URL(privacyLink) : undefined, + ); + expect(config.specialUrls.termsOfUse).toEqual( + termsOfUseLink ? new URL(termsOfUseLink) : undefined, + ); + expect(config.useImageProxy).toEqual(!!imageProxy); + expect(config.version).toEqual( + await getServerVersionFromPackageJson(), + ); + }); + index += 1; } } } diff --git a/src/frontend-config/frontend-config.service.ts b/src/frontend-config/frontend-config.service.ts index d4e1cf31c..df108f86b 100644 --- a/src/frontend-config/frontend-config.service.ts +++ b/src/frontend-config/frontend-config.service.ts @@ -22,7 +22,6 @@ import { AuthProviderType, BrandingDto, FrontendConfigDto, - IframeCommunicationDto, SpecialUrlsDto, } from './frontend-config.dto'; @@ -50,7 +49,6 @@ export class FrontendConfigService { allowRegister: this.authConfig.local.enableRegister, authProviders: this.getAuthProviders(), branding: this.getBranding(), - iframeCommunication: this.getIframeCommunication(), maxDocumentLength: this.noteConfig.maxDocumentLength, plantUmlServer: this.externalServicesConfig.plantUmlServer ? new URL(this.externalServicesConfig.plantUmlServer) @@ -146,11 +144,4 @@ export class FrontendConfigService { : undefined, }; } - - private getIframeCommunication(): IframeCommunicationDto { - return { - editorOrigin: new URL(this.appConfig.domain), - rendererOrigin: new URL(this.appConfig.rendererOrigin), - }; - } }