Skip to content

Commit af24a02

Browse files
committed
fix(@angular/build): Add custom middleware for to present an Angular-tailored message
New Blocked request page for custom host Fix #32028
1 parent 588f3b8 commit af24a02

File tree

5 files changed

+81
-0
lines changed

5 files changed

+81
-0
lines changed

packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { executeDevServer } from '../../index';
1010
import { executeOnceAndGet } from '../execute-fetch';
1111
import { describeServeBuilder } from '../jasmine-helpers';
1212
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
13+
import { text } from 'node:stream/consumers';
1314

1415
const FETCH_HEADERS = Object.freeze({ Host: 'example.com' });
1516

@@ -33,6 +34,7 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT
3334

3435
expect(result?.success).toBeTrue();
3536
expect(response?.statusCode).toBe(403);
37+
expect(response && text(response)).toContain('angular.json');
3638
});
3739

3840
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
4749

4850
expect(result?.success).toBeTrue();
4951
expect(response?.statusCode).toBe(403);
52+
expect(response && text(response)).toContain('angular.json');
5053
});
5154

5255
it('allows a host when specified in the option', async () => {

packages/angular/build/src/builders/dev-server/vite/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ export async function setupServer(
226226
ssrMode,
227227
resetComponentUpdates: () => templateUpdates.clear(),
228228
projectRoot: serverOptions.projectRoot,
229+
allowedHosts: serverOptions.allowedHosts,
230+
devHost: serverOptions.host,
229231
}),
230232
createRemoveIdPrefixPlugin(externalMetadata.explicitBrowser),
231233
await createAngularSsrTransformPlugin(serverOptions.workspaceRoot),
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
export function html403(hostname: string): string {
10+
return `<!doctype html>
11+
<html>
12+
<head>
13+
<meta charset="utf-8" />
14+
<meta name="viewport" content="width=device-width, initial-scale=1" />
15+
<title>Blocked request</title>
16+
<style>
17+
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;
18+
line-height:1.4;margin:2rem;color:#1f2937}
19+
code{background:#f3f4f6;padding:.15rem .35rem;border-radius:.25rem}
20+
.box{max-width:760px;margin:0 auto}
21+
h1{font-size:1.5rem;margin-bottom:.75rem}
22+
p{margin:.5rem 0}
23+
.muted{color:#6b7280}
24+
pre{background:#f9fafb;border:1px solid #e5e7eb;padding:.75rem;border-radius:.5rem;overflow:auto}
25+
</style>
26+
</head>
27+
<body>
28+
<main>
29+
<h1>Blocked request. This host ("${hostname}") is not allowed.</h1>
30+
<p>To allow this host, add it to <code>allowedHosts</code> under the <code>serve</code> target in <code>angular.json</code>.</p>
31+
<pre><code>{
32+
"serve": {
33+
"options": {
34+
"allowedHosts": ["${hostname}"]
35+
}
36+
}
37+
}</code></pre>
38+
</main>
39+
</body>
40+
</html>`;
41+
}

packages/angular/build/src/tools/vite/middlewares/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ export {
1616
export { createAngularHeadersMiddleware } from './headers-middleware';
1717
export { createAngularComponentMiddleware } from './component-middleware';
1818
export { createChromeDevtoolsMiddleware } from './chrome-devtools-middleware';
19+
export { html403 } from './host-check-middleware';

packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import { IncomingMessage, ServerResponse } from 'node:http';
910
import type { Connect, Plugin } from 'vite';
1011
import {
1112
ComponentStyleRecord,
@@ -17,6 +18,7 @@ import {
1718
createAngularSsrExternalMiddleware,
1819
createAngularSsrInternalMiddleware,
1920
createChromeDevtoolsMiddleware,
21+
html403,
2022
} from '../middlewares';
2123
import { AngularMemoryOutputFiles, AngularOutputAssets } from '../utils';
2224

@@ -55,6 +57,8 @@ interface AngularSetupMiddlewaresPluginOptions {
5557
ssrMode: ServerSsrMode;
5658
resetComponentUpdates: () => void;
5759
projectRoot: string;
60+
allowedHosts: true | string[];
61+
devHost: string;
5862
}
5963

6064
async function createEncapsulateStyle(): Promise<
@@ -109,6 +113,36 @@ export function createAngularSetupMiddlewaresPlugin(
109113
// before the built-in HTML middleware
110114
// eslint-disable-next-line @typescript-eslint/no-misused-promises
111115
return async () => {
116+
// Vite/Connect do not expose a typed stack, cast once to a precise structural type.
117+
const entry = server.middlewares.stack.find(
118+
({ handle }) =>
119+
typeof handle === 'function' && handle.name.startsWith('hostValidationMiddleware'),
120+
);
121+
122+
if (typeof entry?.handle === 'function') {
123+
const originalHandle = entry.handle as Connect.NextHandleFunction;
124+
125+
entry.handle = function angularHostValidationMiddleware(
126+
req: IncomingMessage,
127+
res: ServerResponse,
128+
next: (err?: unknown) => void,
129+
) {
130+
originalHandle(
131+
req,
132+
{
133+
writeHead: (code) => {
134+
res.writeHead(code, { 'content-type': 'text/html' });
135+
},
136+
end: () => {
137+
const hostname = req.headers.host?.toLowerCase().split(':')[0] ?? '';
138+
res.end(html403(hostname));
139+
},
140+
} as ServerResponse,
141+
next,
142+
);
143+
};
144+
}
145+
112146
if (ssrMode === ServerSsrMode.ExternalSsrMiddleware) {
113147
server.middlewares.use(
114148
await createAngularSsrExternalMiddleware(server, indexHtmlTransformer),

0 commit comments

Comments
 (0)