diff --git a/README.md b/README.md index 7148fe4..23d6b23 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,24 @@ Note that if you are using a self-signed or otherwise invalid certificate, the browser will not allow the connection by default. Additional browser configs or command line options may be necessary to use an invalid TLS certificate. +## Controlling which network interfaces are listened to + +By default, jasmine-browser-runner listens to all available network interfaces. +You might need that if you're using a remote grid such as Saucelabs. If you +don't need that, you can improve security by listening only to localhost. + +```javascript +export default { + // ... + "listenAddress": "localhost", + // ... +} +``` + ## Hostname support -To serve tests on a specific interface or IP, you can specify a hostname in -`jasmine-browser.mjs`: +If you need to access your tests via a specific hostname, you can do that by +setting the `hostname` configuration property: ```javascript export default { @@ -98,6 +112,11 @@ export default { This can also be specified on the command line with `--hostname`. +Setting `hostname` but not `listenAddress` has the same effect as setting +`listenAddress` to the same value as `hostname`. If you need to set a hostname +but retain the default behavior of listening to all network interfaces, you can +do that by setting `listenAddress` to `"*"`. + There are a few important caveats when doing this: 1. This name must either be an IP or a name that can really be resolved on your diff --git a/lib/server.js b/lib/server.js index 233cfec..3544b25 100644 --- a/lib/server.js +++ b/lib/server.js @@ -224,14 +224,20 @@ class Server { const tlsCert = serverOptions.tlsCert || this.options.tlsCert; const tlsKey = serverOptions.tlsKey || this.options.tlsKey; const hostname = serverOptions.hostname || this.options.hostname; + // The last two fallbacks here are necessary for backwards compatibility. + let listenAddress = + serverOptions.listenAddress || + this.options.listenAddress || + hostname || + ''; + + if (listenAddress === '*') { + listenAddress = ''; + } - // NOTE: Before hostname support, jasmine-browser-runner would listen on - // all IPs (no hostname) and point browsers to "localhost". We preserve - // backward compatibility here by using different defaults for these two - // things. const listenOptions = { port, - host: hostname || '', + host: listenAddress, }; this._httpHostname = hostname || 'localhost'; diff --git a/lib/types.js b/lib/types.js index 734ab7f..2c9d3b0 100644 --- a/lib/types.js +++ b/lib/types.js @@ -52,10 +52,20 @@ * @type string */ /** - * The hostname to use. This influences both the URL given to browsers and the - * addresses on which the socket listens. If blank, for backward - * compatibility, the browsers will be pointed to localhost, but the listening - * socket will listen on all IPs. + * The hostname or IP address of the network interface to listen on. For + * security, this should be set to localhost or an equivalent unless you need + * the server to be accessible over other network interfaces. Set to "*" to + * listen on all interfaces, which may be required by some remote Selenium + * grids. + * @name ServerCtorOptions#listenAddress + * @default The value of {@link ServerCtorOptions#hostname} or {@link ServerStartOptions#hostname} if configured, otherwise "*" + * @type string | undefined + */ +/** + * The hostname to use in the URL given to browsers. For backward compatibility, + * setting this property without also setting {@link ServerCtorOptions#listenAddress} + * or {@link ServerStartOptions#listenAddress} has the same effect as setting + * the listen address to the hostname. * @name ServerCtorOptions#hostname * @default "localhost" * @type string | undefined @@ -307,7 +317,10 @@ /** * @see ServerCtorOptions#hostname * @name ServerStartOptions#hostname - * @type string + */ +/** + * @see ServerCtorOptions#listenAddress + * @name ServerStartOptions#listenAddress */ /** diff --git a/spec/serverSpec.js b/spec/serverSpec.js index 038e036..7d7ae0f 100644 --- a/spec/serverSpec.js +++ b/spec/serverSpec.js @@ -482,10 +482,9 @@ describe('server', function() { } describe('Passing options to the ctor', function() { - describe('When hostname is not specified', function() { + describe('When neither hostname nor listenAddress is specified', function() { it('listens to all interfaces', async function() { const options = baseCtorOptions(); - expect(options.hostname).toBeFalsy(); const http = makeMockNodeServer('http'); this.server = new Server(options, { http }); @@ -498,7 +497,7 @@ describe('server', function() { }); }); - describe('When hostname is specified', function() { + describe('When hostname but not listenAddress is specified', function() { it('listens to the specified hostname', async function() { const options = baseCtorOptions(); options.hostname = 'specific.example.com'; @@ -513,10 +512,59 @@ describe('server', function() { ); }); }); + + describe('When listenAddress but not hostname is specified', function() { + it('listens to the specified listenAddress', async function() { + const options = baseCtorOptions(); + options.listenAddress = 'specific.example.com'; + const http = makeMockNodeServer('http'); + this.server = new Server(options, { http }); + + await this.server.start(); + + expect(http.listen).toHaveBeenCalledWith( + jasmine.objectContaining({ host: 'specific.example.com' }), + jasmine.any(Function) + ); + }); + }); + + describe('When both hostname and listenAddress are specified', function() { + it('listens to the specified listenAddress', async function() { + const options = baseCtorOptions(); + options.listenAddress = 'specific.example.com'; + options.hostname = 'other.example.com'; + const http = makeMockNodeServer('http'); + this.server = new Server(options, { http }); + + await this.server.start(); + + expect(http.listen).toHaveBeenCalledWith( + jasmine.objectContaining({ host: 'specific.example.com' }), + jasmine.any(Function) + ); + }); + }); + + describe('When listenAddress is "*"', function() { + it('listens to all interfaces', async function() { + const options = baseCtorOptions(); + options.listenAddress = '*'; + const http = makeMockNodeServer('http'); + this.server = new Server(options, { http }); + + await this.server.start(); + + expect(http.listen).toHaveBeenCalledWith( + jasmine.objectContaining({ host: '' }), + jasmine.any(Function) + ); + }); + }); }); describe('Passing options to start', function() { - describe('When hostname is not specified', function() { + describe('When neither hostname nor listenAddress is specified', function() { it('listens to all interfaces', async function() { const http = makeMockNodeServer('http'); this.server = new Server(baseCtorOptions(), { http }); @@ -530,7 +578,7 @@ describe('server', function() { }); }); - describe('When hostname is specified', function() { + describe('When hostname but not listenAddress is specified', function() { it('listens to the specified hostname', async function() { const http = makeMockNodeServer('http'); this.server = new Server(baseCtorOptions(), { http }); @@ -543,6 +591,53 @@ describe('server', function() { ); }); }); + + describe('When listenAddress but not hostname is specified', function() { + it('listens to the specified listenAddress', async function() { + const http = makeMockNodeServer('http'); + this.server = new Server(baseCtorOptions(), { http }); + + await this.server.start({ + listenAddress: 'specific.example.com', + }); + + expect(http.listen).toHaveBeenCalledWith( + jasmine.objectContaining({ host: 'specific.example.com' }), + jasmine.any(Function) + ); + }); + }); + + describe('When both hostname and listenAddress are specified', function() { + it('listens to the specified listenAddress', async function() { + const http = makeMockNodeServer('http'); + this.server = new Server(baseCtorOptions(), { http }); + + await this.server.start({ + listenAddress: 'specific.example.com', + hostname: 'other.example.com', + }); + + expect(http.listen).toHaveBeenCalledWith( + jasmine.objectContaining({ host: 'specific.example.com' }), + jasmine.any(Function) + ); + }); + }); + + describe('When listenAddress is "*"', function() { + it('listens to all interfaces', async function() { + const http = makeMockNodeServer('http'); + this.server = new Server(baseCtorOptions(), { http }); + + await this.server.start({ listenAddress: '*' }); + + expect(http.listen).toHaveBeenCalledWith( + jasmine.objectContaining({ host: '' }), + jasmine.any(Function) + ); + }); + }); }); });