Skip to content

Commit eeec444

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

7 files changed

Lines changed: 137 additions & 32 deletions

File tree

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
buildApplicationInternal,
1616
purgeStaleBuildCache,
1717
} from './internal';
18-
import { normalizeOptions } from './options';
18+
import { normalizeOptions, type NormalizedDevServerOptions } from './options';
1919
import type { DevServerBuilderOutput } from './output';
2020
import type { Schema as DevServerBuilderOptions } from './schema';
2121
import { serveWithVite } from './vite';
@@ -75,7 +75,11 @@ async function initialize(
7575
// Purge old build disk cache.
7676
await purgeStaleBuildCache(context);
7777

78-
const normalizedOptions = await normalizeOptions(context, projectName, initialOptions);
78+
const normalizedOptions = (await normalizeOptions(
79+
context,
80+
projectName,
81+
initialOptions,
82+
)) as NormalizedDevServerOptions;
7983
const builderName = await context.getBuilderNameForTarget(normalizedOptions.buildTarget);
8084

8185
if (

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import { Schema as DevServerOptions } from './schema';
1515

1616
export type NormalizedDevServerOptions = Omit<
1717
Awaited<ReturnType<typeof normalizeOptions>>,
18-
'strictPort'
18+
'strictPort' | 'allowedHosts'
1919
> & {
2020
strictPort?: boolean;
21+
allowedHosts: true | string[];
2122
};
2223

2324
/**
@@ -125,8 +126,16 @@ export async function normalizeOptions(
125126
sslKey,
126127
prebundle,
127128
allowedHosts,
129+
disableHostCheck,
128130
} = options;
129131

132+
let normalizedAllowedHosts: true | string[];
133+
if (disableHostCheck === true) {
134+
normalizedAllowedHosts = true;
135+
} else {
136+
normalizedAllowedHosts = Array.isArray(allowedHosts) ? allowedHosts : [];
137+
}
138+
130139
// Return all the normalized options
131140
return {
132141
buildTarget,
@@ -152,6 +161,6 @@ export async function normalizeOptions(
152161
// Prebundling defaults to true but requires caching to function
153162
prebundle: cacheOptions.enabled && !optimization.scripts && prebundle,
154163
inspect,
155-
allowedHosts: allowedHosts ? allowedHosts : [],
164+
allowedHosts: normalizedAllowedHosts,
156165
};
157166
}

‎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+
process.cwd(),
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)