Skip to content

Commit eae16aa

Browse files
committed
fix(@angular/build): allow allowed-hosts CLI option for dev-server
Fixes #33955
1 parent 92bb487 commit eae16aa

6 files changed

Lines changed: 129 additions & 29 deletions

File tree

‎packages/angular/build/src/builders/dev-server/options.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,16 @@ export async function normalizeOptions(
125125
sslKey,
126126
prebundle,
127127
allowedHosts,
128+
disableHostCheck,
128129
} = options;
129130

131+
let normalizedAllowedHosts: true | string[];
132+
if (disableHostCheck === true) {
133+
normalizedAllowedHosts = true;
134+
} else {
135+
normalizedAllowedHosts = Array.isArray(allowedHosts) ? allowedHosts : [];
136+
}
137+
130138
// Return all the normalized options
131139
return {
132140
buildTarget,
@@ -152,6 +160,6 @@ export async function normalizeOptions(
152160
// Prebundling defaults to true but requires caching to function
153161
prebundle: cacheOptions.enabled && !optimization.scripts && prebundle,
154162
inspect,
155-
allowedHosts: allowedHosts ? allowedHosts : [],
163+
allowedHosts: normalizedAllowedHosts,
156164
};
157165
}

‎packages/angular/build/src/builders/dev-server/schema.json‎

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,17 @@
3737
"description": "SSL certificate to use for serving HTTPS."
3838
},
3939
"allowedHosts": {
40-
"description": "The hosts that the development server will respond to. This option sets the Vite option of the same name. For further details: https://vite.dev/config/server-options.html#server-allowedhosts",
40+
"type": "array",
41+
"description": "List of hosts that are allowed to access the dev server.",
4142
"default": [],
42-
"oneOf": [
43-
{
44-
"type": "array",
45-
"description": "A list of hosts that the development server will respond to.",
46-
"items": {
47-
"type": "string"
48-
}
49-
},
50-
{
51-
"type": "boolean",
52-
"description": "Indicates that all hosts are allowed. This is not recommended and a security risk."
53-
}
54-
]
43+
"items": {
44+
"type": "string"
45+
}
46+
},
47+
"disableHostCheck": {
48+
"type": "boolean",
49+
"description": "Don't verify connected clients are part of allowed hosts.",
50+
"default": false
5551
},
5652
"define": {
5753
"description": "Defines global identifiers that will be replaced with a specified constant value when found in any JavaScript or TypeScript code including libraries. The value will be used directly. String values must be put in quotes. Identifiers within Angular metadata such as Component Decorators will not be replaced.",

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

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,5 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT
6565
expect(result?.success).toBeTrue();
6666
expect(content).toContain('<title>');
6767
});
68-
69-
it('allows a host when option is true', async () => {
70-
harness.useTarget('serve', {
71-
...BASE_OPTIONS,
72-
allowedHosts: true,
73-
});
74-
75-
const { result, content } = await executeOnceAndGet(harness, '/', {
76-
request: { headers: FETCH_HEADERS },
77-
});
78-
79-
expect(result?.success).toBeTrue();
80-
expect(content).toContain('<title>');
81-
});
8268
});
8369
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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 { executeDevServer } from '../../index';
10+
import { executeOnceAndGet } from '../execute-fetch';
11+
import { describeServeBuilder } from '../jasmine-helpers';
12+
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
13+
import { text } from 'node:stream/consumers';
14+
15+
const FETCH_HEADERS = Object.freeze({ Host: 'example.com' });
16+
17+
describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => {
18+
describe('option: "disableHostCheck"', () => {
19+
beforeEach(async () => {
20+
setupTarget(harness);
21+
22+
// Application code is not needed for these tests
23+
await harness.writeFile('src/main.ts', '');
24+
});
25+
26+
it('does not allow an invalid host when option is not present', async () => {
27+
harness.useTarget('serve', {
28+
...BASE_OPTIONS,
29+
});
30+
31+
const { result, response } = await executeOnceAndGet(harness, '/', {
32+
request: { headers: FETCH_HEADERS },
33+
});
34+
35+
expect(result?.success).toBeTrue();
36+
expect(response?.statusCode).toBe(403);
37+
expect(response && (await text(response))).toContain('angular.json');
38+
});
39+
40+
it('does not allow an invalid host when option is false', async () => {
41+
harness.useTarget('serve', {
42+
...BASE_OPTIONS,
43+
disableHostCheck: false,
44+
});
45+
46+
const { result, response } = await executeOnceAndGet(harness, '/', {
47+
request: { headers: FETCH_HEADERS },
48+
});
49+
50+
expect(result?.success).toBeTrue();
51+
expect(response?.statusCode).toBe(403);
52+
expect(response && (await text(response))).toContain('angular.json');
53+
});
54+
55+
it('allows a host when option is true', async () => {
56+
harness.useTarget('serve', {
57+
...BASE_OPTIONS,
58+
disableHostCheck: true,
59+
});
60+
61+
const { result, content } = await executeOnceAndGet(harness, '/', {
62+
request: { headers: FETCH_HEADERS },
63+
});
64+
65+
expect(result?.success).toBeTrue();
66+
expect(content).toContain('<title>');
67+
});
68+
});
69+
});

‎packages/angular/cli/BUILD.bazel‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ jasmine_test(
174174
"package.json",
175175
"test-esm-loader.mjs",
176176
":angular-cli_test_lib",
177+
"//packages/angular/build:schemas",
177178
],
178179
node_options = [
179180
"--import=./test-esm-loader.mjs",

‎packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
*/
88

99
import { JsonObject, schema } from '@angular-devkit/core';
10+
import { readFileSync } from 'node:fs';
11+
import path from 'node:path';
1012
import type { Argv } from 'yargs';
1113
import yargs from 'yargs';
1214

@@ -389,4 +391,42 @@ describe('parseJsonSchemaToOptions', () => {
389391
]);
390392
});
391393
});
394+
395+
describe('@angular/build dev-server schema', () => {
396+
const devServerSchemaPath = path.join(
397+
import.meta.dirname,
398+
'../../../../build/src/builders/dev-server/schema.json',
399+
);
400+
401+
const parse = async (args: string[]) => {
402+
return localYargs.parseAsync(args);
403+
};
404+
405+
let localYargs: Argv<unknown>;
406+
407+
beforeEach(async () => {
408+
localYargs = yargs().exitProcess(false).strict().fail(false).wrap(1_000);
409+
410+
const devServerSchema = JSON.parse(readFileSync(devServerSchemaPath, 'utf8')) as JsonObject;
411+
const registry = new schema.CoreSchemaRegistry();
412+
const options = await parseJsonSchemaToOptions(registry, devServerSchema, false);
413+
addSchemaOptionsToCommand(localYargs, options, true);
414+
});
415+
416+
it('parses --allowed-hosts with host names', async () => {
417+
expect(await parse(['--allowed-hosts', 'example.com'])).toEqual(
418+
jasmine.objectContaining({
419+
allowedHosts: ['example.com'],
420+
}),
421+
);
422+
});
423+
424+
it('parses --disable-host-check', async () => {
425+
expect(await parse(['--disable-host-check'])).toEqual(
426+
jasmine.objectContaining({
427+
disableHostCheck: true,
428+
}),
429+
);
430+
});
431+
});
392432
});

0 commit comments

Comments
 (0)