diff --git a/frontend/src/app/(editor)/layout.tsx b/frontend/src/app/(editor)/layout.tsx index 6f20497b2..57c9fb440 100644 --- a/frontend/src/app/(editor)/layout.tsx +++ b/frontend/src/app/(editor)/layout.tsx @@ -9,6 +9,7 @@ import { BaseUrlContextProvider } from '../../components/common/base-url/base-ur import { FrontendConfigContextProvider } from '../../components/common/frontend-config-context/frontend-config-context-provider' import { MotdModal } from '../../components/global-dialogs/motd-modal/motd-modal' import { DarkMode } from '../../components/layout/dark-mode/dark-mode' +import { ExpectedOriginBoundary } from '../../components/layout/expected-origin-boundary' import { UiNotificationBoundary } from '../../components/notifications/ui-notification-boundary' import { StoreProvider } from '../../redux/store-provider' import { baseUrlFromEnvExtractor } from '../../utils/base-url-from-env-extractor' @@ -34,29 +35,29 @@ export default async function RootLayout({ children, appBar }: RootLayoutProps) - - - - - - - -
- {appBar} - {children} -
-
-
-
-
-
+ + + + + + + + +
+ {appBar} + {children} +
+
+
+
+
+
+
) } -export const dynamic = 'force-dynamic' - export const metadata: Metadata = { themeColor: '#b51f08', applicationName: 'HedgeDoc', diff --git a/frontend/src/app/(render)/layout.tsx b/frontend/src/app/(render)/layout.tsx index 05cb4e76b..8afb48915 100644 --- a/frontend/src/app/(render)/layout.tsx +++ b/frontend/src/app/(render)/layout.tsx @@ -7,6 +7,7 @@ import '../../../global-styles/index.scss' import { ApplicationLoader } from '../../components/application-loader/application-loader' import { BaseUrlContextProvider } from '../../components/common/base-url/base-url-context-provider' import { FrontendConfigContextProvider } from '../../components/common/frontend-config-context/frontend-config-context-provider' +import { ExpectedOriginBoundary } from '../../components/layout/expected-origin-boundary' import { StoreProvider } from '../../redux/store-provider' import { baseUrlFromEnvExtractor } from '../../utils/base-url-from-env-extractor' import React from 'react' @@ -19,16 +20,16 @@ export default async function RootLayout({ children }: { children: React.ReactNo return ( - - - - {children} - - - + + + + + {children} + + + + ) } - -export const dynamic = 'force-dynamic' diff --git a/frontend/src/components/layout/expected-origin-boundary.tsx b/frontend/src/components/layout/expected-origin-boundary.tsx new file mode 100644 index 000000000..994cf4a66 --- /dev/null +++ b/frontend/src/components/layout/expected-origin-boundary.tsx @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { headers } from 'next/headers' +import type { PropsWithChildren } from 'react' +import React from 'react' + +export interface ExpectedOriginBoundaryProps extends PropsWithChildren { + expectedOrigin: string +} + +/** + * Determines the host and protocol of the current request by checking the host or x-forwarded-host header. + * + * @return the calculated request origin or {@code undefined} if no host header has been found + */ +export const buildOriginFromHeaders = (): string | undefined => { + const currentHeader = headers() + const host = currentHeader.get('x-forwarded-host') ?? currentHeader.get('host') + if (host === null) { + return undefined + } + + const protocol = currentHeader.get('x-forwarded-proto')?.split(',')[0] ?? 'http' + return `${protocol}://${host}` +} + +/** + * This boundary lets the given children pass if the current request origin is matching the expected origin for this route. + * Otherwise, an error message is shown instead. + * + * @param children the children to show if the origin passes + * @param expectedOrigin The origin that should match the request's origin + */ +export const ExpectedOriginBoundary: React.FC = ({ children, expectedOrigin }) => { + const currentOrigin = buildOriginFromHeaders() + + if (new URL(expectedOrigin).origin !== currentOrigin) { + return {`You can't open this page using this URL. For this endpoint "${expectedOrigin}" is expected.`} + } + return children +} diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts deleted file mode 100644 index 14b6a847b..000000000 --- a/frontend/src/middleware.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { NextResponse } from 'next/server' -import type { NextRequest } from 'next/server' -import { baseUrlFromEnvExtractor } from './utils/base-url-from-env-extractor' - -/** - * Next.js middleware that checks if the expected and the current origin align. - * - * @param request The current request to check - */ -export function middleware(request: NextRequest) { - const currentOrigin = determineOriginFromHeaders(request.headers) - const expectedOrigin = determineExpectedOrigin(request) - - if (currentOrigin === expectedOrigin) { - return - } - - return new NextResponse( - `You can't open this page using this URL. For this endpoint "${expectedOrigin}" is expected.`, - { - status: 400, - headers: { 'content-type': 'text/plain' } - } - ) -} - -/** - * Determines the host and protocol of the current request by checking the host or x-forwarded-host header. - * - * @param headers The request headers provided by Next.js - */ -const determineOriginFromHeaders = (headers: Headers): string | undefined => { - const host = headers.get('x-forwarded-host') ?? headers.get('host') - if (host === null) { - return undefined - } - - const protocol = headers.get('x-forwarded-proto')?.split(',')[0] ?? 'http' - return `${protocol}://${host}` -} - -/** - * Uses the current route to determine the expected origin. - * - * @param request The current request - */ -const determineExpectedOrigin = (request: NextRequest): string => { - return new URL( - request.nextUrl.pathname === '/render' - ? baseUrlFromEnvExtractor.extractBaseUrls().renderer - : baseUrlFromEnvExtractor.extractBaseUrls().editor - ).origin -}