diff --git a/packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts b/packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts index 8e96c7b4b4b0..775e057bece6 100644 --- a/packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts +++ b/packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts @@ -10,6 +10,7 @@ import { executeDevServer } from '../../index'; import { executeOnceAndGet } from '../execute-fetch'; import { describeServeBuilder } from '../jasmine-helpers'; import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup'; +import { text } from 'node:stream/consumers'; const FETCH_HEADERS = Object.freeze({ Host: 'example.com' }); @@ -33,6 +34,7 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT expect(result?.success).toBeTrue(); expect(response?.statusCode).toBe(403); + expect(response && (await text(response))).toContain('angular.json'); }); it('does not allow an invalid host when option is an empty array', async () => { @@ -47,6 +49,7 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT expect(result?.success).toBeTrue(); expect(response?.statusCode).toBe(403); + expect(response && (await text(response))).toContain('angular.json'); }); it('allows a host when specified in the option', async () => { diff --git a/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts new file mode 100644 index 000000000000..8561354812b3 --- /dev/null +++ b/packages/angular/build/src/tools/vite/middlewares/host-check-middleware.ts @@ -0,0 +1,76 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import type { IncomingMessage, ServerResponse } from 'node:http'; +import type { Connect } from 'vite'; + +export function patchHostValidationMiddleware(middlewares: Connect.Server): void { + const entry = middlewares.stack.find( + ({ handle }) => + typeof handle === 'function' && handle.name.startsWith('hostValidationMiddleware'), + ); + + if (typeof entry?.handle !== 'function') { + return; + } + + const originalHandle = entry.handle as Connect.NextHandleFunction; + + entry.handle = function angularHostValidationMiddleware( + req: IncomingMessage, + res: ServerResponse, + next: (err?: unknown) => void, + ) { + originalHandle( + req, + { + writeHead: (code) => { + res.writeHead(code, { 'content-type': 'text/html' }); + }, + end: () => { + const hostname = req.headers.host?.toLowerCase().split(':')[0] ?? ''; + res.end(html403(hostname)); + }, + } as ServerResponse, + next, + ); + }; +} + +function html403(hostname: string): string { + return ` + + + + + Blocked request + + + +
+

Blocked request. This host ("${hostname}") is not allowed.

+

To allow this host, add it to allowedHosts under the serve target in angular.json.

+
{
+  "serve": {
+    "options": {
+      "allowedHosts": ["${hostname}"]
+    }
+  }
+}
+
+ + `; +} diff --git a/packages/angular/build/src/tools/vite/middlewares/index.ts b/packages/angular/build/src/tools/vite/middlewares/index.ts index ef2db01f3aaf..1816fe26265c 100644 --- a/packages/angular/build/src/tools/vite/middlewares/index.ts +++ b/packages/angular/build/src/tools/vite/middlewares/index.ts @@ -16,3 +16,4 @@ export { export { createAngularHeadersMiddleware } from './headers-middleware'; export { createAngularComponentMiddleware } from './component-middleware'; export { createChromeDevtoolsMiddleware } from './chrome-devtools-middleware'; +export { patchHostValidationMiddleware } from './host-check-middleware'; diff --git a/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts b/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts index b82cc2d3acd6..b14c2b409012 100644 --- a/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts +++ b/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts @@ -17,6 +17,7 @@ import { createAngularSsrExternalMiddleware, createAngularSsrInternalMiddleware, createChromeDevtoolsMiddleware, + patchHostValidationMiddleware, } from '../middlewares'; import { AngularMemoryOutputFiles, AngularOutputAssets } from '../utils'; @@ -109,6 +110,8 @@ export function createAngularSetupMiddlewaresPlugin( // before the built-in HTML middleware // eslint-disable-next-line @typescript-eslint/no-misused-promises return async () => { + patchHostValidationMiddleware(server.middlewares); + if (ssrMode === ServerSsrMode.ExternalSsrMiddleware) { server.middlewares.use( await createAngularSsrExternalMiddleware(server, indexHtmlTransformer),