diff --git a/docs/documentation/serve.md b/docs/documentation/serve.md index 0de32c40c72e..3a237ed8ff60 100644 --- a/docs/documentation/serve.md +++ b/docs/documentation/serve.md @@ -39,12 +39,22 @@ All the build Options are available in serve, below are the additional options.
- live-reload-client + public-host

- --live-reload-client + --public-host (aliases: --live-reload-client)

- Specify the URL that the live reload browser client will use. + Specify the URL that the browser client will use. +

+
+ +
+ disable-host-check +

+ --disable-host-check default value: false +

+

+ Don't verify connected clients are part of allowed hosts.

diff --git a/package.json b/package.json index 7b97b41b343f..ba488a712e4b 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "walk-sync": "^0.3.1", "webpack": "~2.4.0", "webpack-dev-middleware": "^1.10.2", - "webpack-dev-server": "~2.4.2", + "webpack-dev-server": "~2.4.5", "webpack-merge": "^2.4.0", "zone.js": "^0.8.4" }, diff --git a/packages/@angular/cli/commands/serve.ts b/packages/@angular/cli/commands/serve.ts index 01254c523145..3d3169f2ce1d 100644 --- a/packages/@angular/cli/commands/serve.ts +++ b/packages/@angular/cli/commands/serve.ts @@ -20,7 +20,8 @@ export interface ServeTaskOptions extends BuildOptions { host?: string; proxyConfig?: string; liveReload?: boolean; - liveReloadClient?: string; + publicHost?: string; + disableHostCheck?: boolean; ssl?: boolean; sslKey?: string; sslCert?: string; @@ -84,9 +85,16 @@ export const baseServeCommandOptions: any = overrideOptions([ description: 'Whether to reload the page on change, using live-reload.' }, { - name: 'live-reload-client', + name: 'public-host', type: String, - description: 'Specify the URL that the live reload browser client will use.' + aliases: ['live-reload-client'], + description: 'Specify the URL that the browser client will use.' + }, + { + name: 'disable-host-check', + type: Boolean, + default: false, + description: 'Don\'t verify connected clients are part of allowed hosts.', }, { name: 'hmr', diff --git a/packages/@angular/cli/custom-typings.d.ts b/packages/@angular/cli/custom-typings.d.ts index 3fdc301c3fc5..91c843700857 100644 --- a/packages/@angular/cli/custom-typings.d.ts +++ b/packages/@angular/cli/custom-typings.d.ts @@ -21,4 +21,6 @@ interface IWebpackDevServerConfigurationOptions { key?: string; cert?: string; overlay?: boolean | { errors: boolean, warnings: boolean }; + public?: string; + disableHostCheck?: boolean; } diff --git a/packages/@angular/cli/package.json b/packages/@angular/cli/package.json index 262ad0944d4e..abae410b7ca8 100644 --- a/packages/@angular/cli/package.json +++ b/packages/@angular/cli/package.json @@ -80,7 +80,7 @@ "walk-sync": "^0.3.1", "webpack": "~2.4.0", "webpack-dev-middleware": "^1.10.2", - "webpack-dev-server": "~2.4.2", + "webpack-dev-server": "~2.4.5", "webpack-merge": "^2.4.0", "zone.js": "^0.8.4" }, diff --git a/packages/@angular/cli/tasks/serve.ts b/packages/@angular/cli/tasks/serve.ts index f323136b1104..e0860ec498e0 100644 --- a/packages/@angular/cli/tasks/serve.ts +++ b/packages/@angular/cli/tasks/serve.ts @@ -49,9 +49,18 @@ export default Task.extend({ hostname: serveTaskOptions.host === '0.0.0.0' ? 'localhost' : serveTaskOptions.host, port: serveTaskOptions.port.toString() }); + + if (serveTaskOptions.disableHostCheck) { + ui.writeLine(oneLine` + ${chalk.yellow('WARNING')} Running a server with --disable-host-check is a security risk. + See https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a + for more information. + `); + } + let clientAddress = serverAddress; - if (serveTaskOptions.liveReloadClient) { - const clientUrl = url.parse(serveTaskOptions.liveReloadClient); + if (serveTaskOptions.publicHost) { + const clientUrl = url.parse(serveTaskOptions.publicHost); // very basic sanity check if (!clientUrl.host) { return Promise.reject(new SilentError(`'live-reload-client' must be a full URL.`)); @@ -96,7 +105,7 @@ export default Task.extend({ webpackConfig.plugins.unshift({ apply: (compiler: any) => { compiler.plugin('after-environment', () => { - compiler.watchFileSystem = { watch: () => {} }; + compiler.watchFileSystem = { watch: () => { } }; }); } }); @@ -153,7 +162,9 @@ export default Task.extend({ errors: serveTaskOptions.target === 'development', warnings: false }, - contentBase: false + contentBase: false, + public: serveTaskOptions.publicHost, + disableHostCheck: serveTaskOptions.disableHostCheck }; if (sslKey != null && sslCert != null) { @@ -193,7 +204,7 @@ export default Task.extend({ return reject(err); } if (serveTaskOptions.open) { - opn(serverAddress); + opn(serverAddress); } }); }) diff --git a/tests/e2e/tests/misc/live-reload.ts b/tests/e2e/tests/misc/live-reload.ts index bcd2fa799c81..e3a9346dc58a 100644 --- a/tests/e2e/tests/misc/live-reload.ts +++ b/tests/e2e/tests/misc/live-reload.ts @@ -124,7 +124,7 @@ export default function () { // Serve with live reload client set to api should call api. .then(_ => silentExecAndWaitForOutputToMatch( 'ng', - ['e2e', '--watch', `--live-reload-client=${apiUrl}`], + ['e2e', '--watch', `--public-host=${apiUrl}`], protractorGoodRegEx )) .then(_ => wait(2000)) diff --git a/tests/e2e/tests/misc/public-host.ts b/tests/e2e/tests/misc/public-host.ts new file mode 100644 index 000000000000..c566841d5c1d --- /dev/null +++ b/tests/e2e/tests/misc/public-host.ts @@ -0,0 +1,43 @@ +import * as os from 'os'; +import * as _ from 'lodash'; + +import { request } from '../../utils/http'; +import { killAllProcesses } from '../../utils/process'; +import { ngServe } from '../../utils/project'; + +export default function () { + const firstLocalIp = _(os.networkInterfaces()) + .values() + .flatten() + .filter({ family: 'IPv4', internal: false }) + .map('address') + .first(); + const publicHost = `${firstLocalIp}:4200`; + const localAddress = `http://${publicHost}`; + + return Promise.resolve() + .then(() => ngServe('--host=0.0.0.0')) + .then(() => request(localAddress)) + .then(body => { + if (!body.match(/Invalid Host header/)) { + throw new Error('Response does not match expected value.'); + } + }) + .then(() => killAllProcesses(), (err) => { killAllProcesses(); throw err; }) + .then(() => ngServe('--host=0.0.0.0', `--public-host=${publicHost}`)) + .then(() => request(localAddress)) + .then(body => { + if (!body.match(/<\/app-root>/)) { + throw new Error('Response does not match expected value.'); + } + }) + .then(() => killAllProcesses(), (err) => { killAllProcesses(); throw err; }) + .then(() => ngServe('--host=0.0.0.0', `--disable-host-check`)) + .then(() => request(localAddress)) + .then(body => { + if (!body.match(/<\/app-root>/)) { + throw new Error('Response does not match expected value.'); + } + }) + .then(() => killAllProcesses(), (err) => { killAllProcesses(); throw err; }); +}