From 8e5bee6e20e4e1ce2f032c7dbe4d5c9e461bda5a Mon Sep 17 00:00:00 2001 From: grifball Date: Sun, 11 Dec 2016 04:20:52 -0800 Subject: [PATCH] feat(server): add listen address option so that IPv6 and loopback interfaces can be used Fixes #2477 --- docs/config/01-configuration-file.md | 6 ++++++ lib/config.js | 24 ++++++++++++++++++++++++ lib/constants.js | 1 + lib/server.js | 6 +++--- test/unit/server.spec.js | 23 ++++++++++++++++++++++- 5 files changed, 56 insertions(+), 4 deletions(-) diff --git a/docs/config/01-configuration-file.md b/docs/config/01-configuration-file.md index 69939c78f..12641af02 100644 --- a/docs/config/01-configuration-file.md +++ b/docs/config/01-configuration-file.md @@ -368,6 +368,12 @@ Please note just about all frameworks in Karma require an additional plugin/fram Additional information can be found in [plugins]. +## listenAddress +**Type:** String + +**Default:** `'0.0.0.0' or LISTEN_ADDR` + +**Description:** Address that the server will listen on. Change to 'localhost' to only listen to the loopback, or '::' to listen on all IPv6 interfaces ## hostname **Type:** String diff --git a/lib/config.js b/lib/config.js index 008e981b0..089125bcf 100644 --- a/lib/config.js +++ b/lib/config.js @@ -280,6 +280,7 @@ var Config = function () { this.frameworks = [] this.protocol = 'http:' this.port = constant.DEFAULT_PORT + this.listenAddress = constant.DEFAULT_LISTEN_ADDR this.hostname = constant.DEFAULT_HOSTNAME this.httpsServerConfig = {} this.basePath = '' @@ -372,6 +373,15 @@ var parseConfig = function (configFilePath, cliOptions) { } var config = new Config() + + // save and reset hostname and listenAddress so we can detect if the user + // changed them + var defaultHostname = config.hostname + config.hostname = null + var defaultListenAddress = config.listenAddress + config.listenAddress = null + + // add the user's configuration in config.set(cliOptions) try { @@ -384,6 +394,20 @@ var parseConfig = function (configFilePath, cliOptions) { // merge the config from config file and cliOptions (precedence) config.set(cliOptions) + // if the user changed listenAddress, but didn't set a hostname, warn them + if (config.hostname === null && config.listenAddress !== null) { + log.warn('ListenAddress was set to %s but hostname was left as the default: ' + + '%s. If your browsers fail to connect, consider changing the hostname option.', + config.listenAddress, defaultHostname) + } + // restore values that weren't overwritten by the user + if (config.hostname === null) { + config.hostname = defaultHostname + } + if (config.listenAddress === null) { + config.listenAddress = defaultListenAddress + } + // configure the logger as soon as we can logger.setup(config.logLevel, config.colors, config.loggers) diff --git a/lib/constants.js b/lib/constants.js index 6008a7309..6da52abd0 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -7,6 +7,7 @@ exports.VERSION = pkg.version exports.DEFAULT_PORT = process.env.PORT || 9876 exports.DEFAULT_HOSTNAME = process.env.IP || 'localhost' +exports.DEFAULT_LISTEN_ADDR = process.env.LISTEN_ADDR || '0.0.0.0' // log levels exports.LOG_DISABLE = 'OFF' diff --git a/lib/server.js b/lib/server.js index 0c91bdbf3..b68631b0e 100644 --- a/lib/server.js +++ b/lib/server.js @@ -160,7 +160,7 @@ Server.prototype._start = function (config, launcher, preprocess, fileList, if (e.code === 'EADDRINUSE') { self.log.warn('Port %d in use', config.port) config.port++ - webServer.listen(config.port) + webServer.listen(config.port, config.listenAddress) } else { throw e } @@ -171,9 +171,9 @@ Server.prototype._start = function (config, launcher, preprocess, fileList, self._injector.invoke(watcher.watch) } - webServer.listen(config.port, function () { + webServer.listen(config.port, config.listenAddress, function () { self.log.info('Karma v%s server started at %s//%s:%s%s', constant.VERSION, - config.protocol, config.hostname, config.port, config.urlRoot) + config.protocol, config.listenAddress, config.port, config.urlRoot) self.emit('listening', config.port) if (config.browsers && config.browsers.length) { diff --git a/test/unit/server.spec.js b/test/unit/server.spec.js index 722cb80f7..dc86788fd 100644 --- a/test/unit/server.spec.js +++ b/test/unit/server.spec.js @@ -27,6 +27,7 @@ describe('server', () => { {frameworks: [], port: 9876, autoWatch: true, + listenAddress: '127.0.0.1', hostname: 'localhost', urlRoot: '/', browsers: ['fake'], @@ -77,7 +78,13 @@ describe('server', () => { webServerOnError = handler } }, - listen: sinon.spy((port, callback) => { + listen: sinon.spy((port, arg2, arg3) => { + var callback = null + if (typeof arg2 === 'function') { + callback = arg2 + } else if (typeof arg3 === 'function') { + callback = arg3 + } callback && callback() }), removeAllListeners: () => {}, @@ -135,6 +142,20 @@ describe('server', () => { expect(server._injector.invoke).to.have.been.calledWith(mockLauncher.launch, mockLauncher) }) + it('should listen on the listenAddress in the config', () => { + server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneSpy) + + expect(mockWebServer.listen).not.to.have.been.called + expect(webServerOnError).not.to.be.null + + expect(mockConfig.listenAddress).to.be.equal('127.0.0.1') + + fileListOnResolve() + + expect(mockWebServer.listen).to.have.been.calledWith(9876, '127.0.0.1') + expect(mockConfig.listenAddress).to.be.equal('127.0.0.1') + }) + it('should try next port if already in use', () => { server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, doneSpy)