Skip to content

Commit 30b5d81

Browse files
authored
fix(@angular/build): Add custom middleware for to present an Angular-tailored message
New Blocked request page for custom host
1 parent 4cdc66b commit 30b5d81

File tree

4 files changed

+83
-0
lines changed

4 files changed

+83
-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 && (await 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 && (await text(response))).toContain('angular.json');
5053
});
5154

5255
it('allows a host when specified in the option', async () => {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
import type { IncomingMessage, ServerResponse } from 'node:http';
10+
import type { Connect } from 'vite';
11+
12+
export function patchHostValidationMiddleware(middlewares: Connect.Server): void {
13+
const entry = middlewares.stack.find(
14+
({ handle }) =>
15+
typeof handle === 'function' && handle.name.startsWith('hostValidationMiddleware'),
16+
);
17+
18+
if (typeof entry?.handle !== 'function') {
19+
return;
20+
}
21+
22+
const originalHandle = entry.handle as Connect.NextHandleFunction;
23+
24+
entry.handle = function angularHostValidationMiddleware(
25+
req: IncomingMessage,
26+
res: ServerResponse,
27+
next: (err?: unknown) => void,
28+
) {
29+
originalHandle(
30+
req,
31+
{
32+
writeHead: (code) => {
33+
res.writeHead(code, { 'content-type': 'text/html' });
34+
},
35+
end: () => {
36+
const hostname = req.headers.host?.toLowerCase().split(':')[0] ?? '';
37+
res.end(html403(hostname));
38+
},
39+
} as ServerResponse,
40+
next,
41+
);
42+
};
43+
}
44+
45+
function html403(hostname: string): string {
46+
return `<!doctype html>
47+
<html>
48+
<head>
49+
<meta charset="utf-8" />
50+
<meta name="viewport" content="width=device-width, initial-scale=1" />
51+
<title>Blocked request</title>
52+
<style>
53+
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;
54+
line-height:1.4;margin:2rem;color:#1f2937}
55+
code{background:#f3f4f6;padding:.15rem .35rem;border-radius:.25rem}
56+
main{max-width:760px;margin:0 auto}
57+
h1{font-size:1.5rem;margin-bottom:.75rem}
58+
p{margin:.5rem 0}
59+
pre{background:#f9fafb;border:1px solid #e5e7eb;padding:.75rem;border-radius:.5rem;overflow:auto}
60+
</style>
61+
</head>
62+
<body>
63+
<main>
64+
<h1>Blocked request. This host ("${hostname}") is not allowed.</h1>
65+
<p>To allow this host, add it to <code>allowedHosts</code> under the <code>serve</code> target in <code>angular.json</code>.</p>
66+
<pre><code>{
67+
"serve": {
68+
"options": {
69+
"allowedHosts": ["${hostname}"]
70+
}
71+
}
72+
}</code></pre>
73+
</main>
74+
</body>
75+
</html>`;
76+
}

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 { patchHostValidationMiddleware } from './host-check-middleware';

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
createAngularSsrExternalMiddleware,
1818
createAngularSsrInternalMiddleware,
1919
createChromeDevtoolsMiddleware,
20+
patchHostValidationMiddleware,
2021
} from '../middlewares';
2122
import { AngularMemoryOutputFiles, AngularOutputAssets } from '../utils';
2223

@@ -109,6 +110,8 @@ export function createAngularSetupMiddlewaresPlugin(
109110
// before the built-in HTML middleware
110111
// eslint-disable-next-line @typescript-eslint/no-misused-promises
111112
return async () => {
113+
patchHostValidationMiddleware(server.middlewares);
114+
112115
if (ssrMode === ServerSsrMode.ExternalSsrMiddleware) {
113116
server.middlewares.use(
114117
await createAngularSsrExternalMiddleware(server, indexHtmlTransformer),

0 commit comments

Comments
 (0)